react-native-screen-transitions 1.1.0 → 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,23 +1,28 @@
1
1
  # react-native-screen-transitions
2
2
 
3
3
 
4
- | iOS | Android |
5
- |---------|---------|
6
- |<video src="https://github.com/user-attachments/assets/8a7b8006-f165-4a78-b0f9-c94cddd948b9" width="300" controls></video>|<video src="https://github.com/user-attachments/assets/ddebdaa8-a929-43ab-b857-08a00e142343" width="300" controls></video>|
7
4
 
8
5
 
9
- **WIP**: This package is a work-in-progress. It provides customizable screen transition animations for React Native apps, primarily designed for use with `expo-router` and `react-navigation`. It supports gestures, predefined presets, and custom animations, making it easy to add polished transitions to your navigation flows.
10
6
 
11
- This library is inspired by the transition system in `@react-navigation/stack` (not the native stack). If you're familiar with how transitions work there (e.g., using interpolators), you'll find this similar.
12
7
 
13
8
 
14
9
 
15
10
 
16
- ## Features
17
- - Predefined animation presets (e.g., SlideFromTop, ZoomIn, DraggableCard).
18
- - Gesture support for interactive transitions (e.g., drag-to-dismiss).
19
- - Animations using Reanimated.
20
- - Easy integration with expo-router and react-navigation.
11
+
12
+
13
+
14
+
15
+
16
+
17
+
18
+
19
+
20
+ | iOS | Android |
21
+ |---|---|
22
+ | <video src="https://github.com/user-attachments/assets/81f39391-80c0-4ce4-b6ff-76de85d2cf03" width="300" height="600" controls></video> | <video src="https://github.com/user-attachments/assets/c2b4c6ca-2b0c-4cf4-a164-f7e68cee0c32" width="300" controls></video> |
23
+
24
+
25
+ **WIP**: This package is a work-in-progress. It provides customizable screen transition animations for React Native apps, primarily designed for use with `expo-router` and `react-navigation`. It supports gestures, predefined presets, and custom animations, making it easy to add polished transitions to your navigation flows.
21
26
 
22
27
  ## Compatibility
23
28
  - **Platforms**: Currently tested on iOS and Android. Not tested or intended for web—web support is not a priority and may not work due to gesture and animation differences.
@@ -34,214 +39,197 @@ bun add react-native-screen-transitions
34
39
 
35
40
  ## Peer Dependencies
36
41
  ```bash
37
- npm install react-native-reanimated react-native-gesture-handler
42
+ npm install react-native-reanimated react-native-gesture-handler @react-navigation/native-stack
38
43
  ```
39
44
 
40
- ## Your First Screen Transition
41
-
42
- Getting started with screen transitions is simple. Here's how to add your first animated transition:
43
-
44
- ### 1. Wrap your app with GestureHandlerRootView
45
+ ## Setup
45
46
 
47
+ ### 1. React Navigation
46
48
  ```tsx
47
- // app/_layout.tsx (expo-router) or App.tsx (react-navigation)
48
- import { GestureHandlerRootView } from "react-native-gesture-handler";
49
- import Transition from "react-native-screen-transitions";
49
+ import { NavigationContainer } from '@react-navigation/native';
50
+ import { GestureHandlerRootView } from 'react-native-gesture-handler';
51
+ import Transition from 'react-native-screen-transitions';
52
+
53
+ const Stack = Transition.createTransitionableStackNavigator();
50
54
 
51
- export default function RootLayout() {
55
+ export default function App() {
52
56
  return (
53
- <GestureHandlerRootView style={{ flex: 1 }}>
54
- {/* Your navigation setup */}
57
+ <GestureHandlerRootView>
58
+ <NavigationContainer>
59
+ <Stack.Navigator>
60
+ <Stack.Screen
61
+ name="Home"
62
+ component={Home}
63
+ options={{
64
+ skipDefaultScreenOptions: true, // prevents transparent-modal default
65
+ }}
66
+ />
67
+ <Stack.Screen
68
+ name="A"
69
+ component={ScreenA}
70
+ options={{
71
+ ...Transition.presets.SlideFromTop(),
72
+ }}
73
+ />
74
+ </Stack.Navigator>
75
+ </NavigationContainer>
55
76
  </GestureHandlerRootView>
56
77
  );
57
78
  }
58
79
  ```
59
80
 
60
- ### 2. Add transition configuration to your screens
81
+ ### 2. Expo Router
82
+ Use the withLayoutContext to convert it into an expo router compatible navigator.
61
83
 
62
84
  ```tsx
63
- import { Stack } from "expo-router";
64
- import Transition from "react-native-screen-transitions";
85
+ import { withLayoutContext } from 'expo-router';
86
+ import Transition, {
87
+ type TransitionStackNavigatorTypeBag,
88
+ } from 'react-native-screen-transitions';
89
+
90
+ const TransitionableNativeStack =
91
+ Transition.createTransitionableStackNavigator();
92
+
93
+ export const Stack = withLayoutContext<
94
+ TransitionStackNavigatorTypeBag['ScreenOptions'],
95
+ typeof TransitionableNativeStack.Navigator,
96
+ TransitionStackNavigatorTypeBag['State'],
97
+ TransitionStackNavigatorTypeBag['EventMap']
98
+ >(TransitionableNativeStack.Navigator);
99
+ ```
65
100
 
66
- export default function RootLayout() {
67
- return (
101
+ Use it exactly like any other Expo Router layout:
102
+
103
+ ```tsx
104
+ import { Stack } from './layouts/stack.tsx'
105
+
106
+ export default function RootLayout(){
107
+ return(
68
108
  <GestureHandlerRootView>
69
109
  <Stack>
70
110
  <Stack.Screen
71
111
  name="a"
72
- options={{ headerShown: false }}
73
- {...Transition.createScreenConfig()}
112
+ options={{
113
+ // You usually don't want your first screen to be a transparent modal.
114
+ skipDefaultScreenOptions: true,
115
+ }}
74
116
  />
75
117
  <Stack.Screen
76
118
  name="b"
77
- {...Transition.createScreenConfig({
119
+ options={{
78
120
  ...Transition.presets.SlideFromTop(),
79
- })}
121
+ }}
80
122
  />
81
123
  </Stack>
82
124
  </GestureHandlerRootView>
83
- );
125
+ )
84
126
  }
85
127
  ```
86
128
 
87
- > ⚠️ **Important**: The first screen (like "a" in the example) must include `{...Transition.createScreenConfig()}` for it to be properly controlled by incoming screens.
129
+ > **Note**: `Transition.createTransitionableStackNavigator()` returns a Native Stack Navigator that's been injected with the necessary functionality for screen transitions to work.
88
130
 
89
- ### 3. Use transition-aware components in your screens
90
131
 
91
- ```tsx
92
- // a.tsx
93
- import Transition from "react-native-screen-transitions";
132
+ ## Creating your screen animations
94
133
 
95
- export default function A() {
96
- return (
97
- <Transition.View> {/* By default has flex: 1 */}
98
- {/* Your content */}
99
- </Transition.View>
100
- );
101
- }
134
+ ### Using presets
102
135
 
103
- // b.tsx
104
- import Transition from "react-native-screen-transitions";
136
+ Pick a built-in preset and spread it into the screen’s options.
137
+ The incoming screen automatically controls the previous screen.
105
138
 
106
- export default function B() {
107
- return (
108
- <Transition.View>
109
- {/* Your content */}
110
- </Transition.View>
111
- );
112
- }
139
+ ```tsx
140
+ <Stack>
141
+ <Stack.Screen
142
+ name="a"
143
+ options={{
144
+ // avoids transparent-modal default for first screen
145
+ skipDefaultScreenOptions: true,
146
+ }}
147
+ />
148
+ <Stack.Screen
149
+ name="b"
150
+ options={{
151
+ ...Transition.presets.SlideFromTop(),
152
+ }}
153
+ />
154
+ <Stack.Screen
155
+ name="c"
156
+ options={{
157
+ ...Transition.presets.SlideFromBottom(),
158
+ }}
159
+ />
160
+ </Stack>
113
161
  ```
114
162
 
115
- ### For Nested Navigators
116
-
117
- When using nested navigators, wrap the nested Stack in a transition-aware component:
163
+ > ⚠️ **Important**
164
+ > Any screen that **must** participate in a transition (i.e., be animated) **must** be wrapped in a transition-aware component.
165
+ > For example, if both `a` and `b` are meant to animate, wrap each screen’s root like this:
118
166
 
119
167
  ```tsx
120
- // app/nested/_layout.tsx
121
- import Transition from "react-native-screen-transitions";
168
+ // a.tsx
169
+ <Transition.View>
170
+ ...
171
+ </Transition.View>
122
172
 
123
- export default function TabLayout() {
124
- return (
125
- <Transition.View>
126
- <Stack>
127
- <Stack.Screen name="nested-a" {...Transition.createScreenConfig()} />
128
- <Stack.Screen name="nested-b" {...Transition.createScreenConfig()} />
129
- </Stack>
130
- </Transition.View>
131
- );
132
- }
173
+ // b.tsx
174
+ <Transition.View>
175
+ ...
176
+ </Transition.View>
133
177
  ```
134
178
 
135
- ## Advanced Usage
179
+ Without this wrapper, the transition system cannot animate the screen and the animation will appear broken or skipped.
136
180
 
137
- ### Method 1: Navigator-Level Interpolator (Recommended)
138
181
 
139
- Define a `screenStyleInterpolator` at the navigator level to animate both entering and exiting screens simultaneously. This approach is much cleaner.
140
182
 
141
- ```tsx
142
- // app/_layout.tsx
143
- import { Stack } from "expo-router";
144
- import { GestureHandlerRootView } from "react-native-gesture-handler";
145
- import Transition from "react-native-screen-transitions";
146
- import { interpolate, Easing } from "react-native-reanimated";
183
+ ### Navigator-level custom animations
147
184
 
148
- export default function RootLayout() {
149
- return (
150
- <GestureHandlerRootView>
151
- <Stack>
152
- <Stack.Screen
153
- name="a"
154
- {...Transition.createScreenConfig()} // Initialize
155
- />
156
- <Stack.Screen
157
- name="b"
158
- {...Transition.createScreenConfig({
159
- gestureDirection: "horizontal",
160
- gestureEnabled: true,
161
- gestureResponseDistance: 50,
162
- gestureVelocityImpact: 0.3,
163
- screenStyleInterpolator: ({
164
- current,
165
- next,
166
- layouts: { screen: { width } },
167
- }) => {
168
- "worklet";
169
- // Mimics the iOS stack slide animation
170
- const progress = current.progress.value + (next?.progress.value || 0);
171
- const translateX = interpolate(
172
- progress,
173
- [0, 1, 2],
174
- [width, 0, width * -0.3],
175
- "clamp"
176
- );
177
- return {
178
- contentStyle: {
179
- transform: [{ translateX }],
180
- },
181
- };
182
- },
183
- transitionSpec: {
184
- open: {
185
- easing: Easing.bezier(0.25, 0.1, 0.25, 1.0),
186
- duration: 1000,
187
- },
188
- close: {
189
- damping: 10,
190
- mass: 0.5,
191
- stiffness: 100,
192
- },
193
- },
194
- })}
195
- />
196
- </Stack>
197
- </GestureHandlerRootView>
198
- );
199
- }
200
- ```
201
-
202
- When using `screenStyleInterpolator`, both screens must wrap their content in a transition-aware component.
203
-
204
- ### Method 2: Screen-Level Animations
205
-
206
- Alternatively, define animations at the screen level using the `useScreenAnimation` hook. This is useful for screen-specific effects or when you don't need to animate both screens. You CAN combine this with `screenStyleInterpolator` for more advanced animations, but for this example, we'll leave `screenStyleInterpolator` undefined.
185
+ Instead of presets, you can define a custom transition directly on the screen’s options.
186
+ The `screenStyleInterpolator` receives the current and next screen’s progress and lets you animate both at once.
207
187
 
208
188
  ```tsx
209
- // app/_layout.tsx
210
- <Stack.Screen
211
- name="a"
212
- {...Transition.createScreenConfig()}
213
- />
189
+ import { interpolate } from 'react-native-reanimated'
214
190
  <Stack.Screen
215
191
  name="b"
216
- {...Transition.createScreenConfig({
192
+ options={{
193
+ screenStyleInterpolator: ({
194
+ current,
195
+ next,
196
+ layouts: { screen: { width } },
197
+ }) => {
198
+ "worklet";
199
+
200
+ const progress = current.progress.value + (next?.progress.value ?? 0);
201
+
202
+ const x = interpolate(progress, [0, 1, 2], [width, 0, -width]);
203
+ return {
204
+ contentStyle: {
205
+ transform: [{ translateX: x }],
206
+ },
207
+ };
208
+ },
217
209
  transitionSpec: {
218
- open: {
219
- easing: Easing.bezier(0.25, 0.1, 0.25, 1.0),
220
- duration: 1000,
221
- },
222
- close: {
223
- damping: 10,
224
- mass: 0.5,
225
- stiffness: 100,
226
- },
210
+ close: Transition.specs.DefaultSpec,
211
+ open: Transition.specs.DefaultSpec,
227
212
  },
228
- })}
213
+ }}
229
214
  />
230
215
  ```
231
216
 
217
+ In this example the incoming screen slides in from the right while the exiting screen slides out to the left.
218
+
219
+ ### Screen-level custom animations with `useScreenAnimation`
220
+
221
+ For per-screen control, import the `useScreenAnimation` hook and compose your own animated styles.
222
+
232
223
  ```tsx
233
- // a.tsx (previous screen)
234
224
  import { useScreenAnimation } from 'react-native-screen-transitions';
235
225
  import Animated, { useAnimatedStyle, interpolate } from 'react-native-reanimated';
236
226
 
237
- export default function A() {
238
- const { next, layouts: { screen: { width } } } = useScreenAnimation();
227
+ export default function BScreen() {
228
+ const { current } = useScreenAnimation();
239
229
 
240
230
  const animatedStyle = useAnimatedStyle(() => {
241
- // Unfocusing animation - screen slides left when next screen enters
242
- const translateX = interpolate(next?.progress.value || 0, [0, 1], [0, width * -0.3]);
243
231
  return {
244
- transform: [{ translateX }],
232
+ opacity: current.progress.value
245
233
  };
246
234
  });
247
235
 
@@ -251,139 +239,99 @@ export default function A() {
251
239
  </Animated.View>
252
240
  );
253
241
  }
242
+ ```
254
243
 
255
- // b.tsx (entering screen)
256
- import { useScreenAnimation } from 'react-native-screen-transitions';
257
- import Animated, { useAnimatedStyle, interpolate } from 'react-native-reanimated';
244
+ ## Apply screen transitions to nested navigators
258
245
 
259
- export default function B() {
260
- const { current, layouts: { screen: { width } } } = useScreenAnimation();
246
+ When a screen contains its own stack (e.g., `b` is a nested navigator), wrap the nested `Stack` in a `Transition.View`.
261
247
 
262
- const animatedStyle = useAnimatedStyle(() => {
263
- // Focusing animation - screen slides in from right
264
- const translateX = interpolate(current.progress.value, [0, 1], [width, 0]);
265
- return {
266
- transform: [{ translateX }],
267
- };
268
- });
248
+ ```tsx
249
+ // app/b/_layout.tsx (nested stack for screen "b")
250
+ import { Stack } from 'expo-router';
251
+ import Transition from 'react-native-screen-transitions';
269
252
 
253
+ export default function BLayout() {
270
254
  return (
271
- <Animated.View style={[{ flex: 1 }, animatedStyle]}>
272
- {/* Your content */}
273
- </Animated.View>
255
+ <Transition.View>
256
+ <Stack>
257
+ <Stack.Screen name="index" options={{ headerShown: false }} />
258
+ <Stack.Screen name="details" options={{ headerShown: false }} />
259
+ </Stack>
260
+ </Transition.View>
274
261
  );
275
262
  }
276
263
  ```
277
264
 
265
+ The outer transition now treats the entire nested stack as a single animatable view while each inner screen can still function normally—whether you keep the native stack or swap it for the Transitionable Stack.
278
266
 
267
+ ## Swipe-to-dismiss with scrollables
279
268
 
280
- ## Dismissible Screens with Scrollables
281
-
282
- Screen transitions can be dismissed based on defined gesture directions. Integration with scrollable components is seamless:
283
-
284
- ### Create Custom Transition-Aware Scrollables
285
-
286
- You can use built-in scrollables or create your own
269
+ You can drag a screen away even when it contains a scroll view.
270
+ Just swap the regular scrollable for a transition-aware one:
287
271
 
288
272
  ```tsx
273
+ import Transition from 'react-native-screen-transitions';
274
+ import { LegendList } from "@legendapp/list"
289
275
  import { FlashList } from "@shopify/flash-list";
290
- import { LegendList } from "@legendapp/list";
291
- import { FlatList, ScrollView } from "react-native";
292
- import Transition from "react-native-screen-transitions";
293
276
 
294
- // Built-in transition-aware scrollables
295
- const MyScrollView = Transition.ScrollView;
296
- const MyFlatList = Transition.FlatList;
277
+ // Drop-in replacements
278
+ const ScrollView = Transition.ScrollView;
279
+ const FlatList = Transition.FlatList;
297
280
 
298
- // Create custom transition-aware scrollables
299
- const TransitionFlashList = Transition.createTransitionAwareScrollable(FlashList);
300
- const TransitionLegendList = Transition.createTransitionAwareScrollable(LegendList);
301
- ```
281
+ // Or wrap any list you like
282
+ const TransitionFlashList =
283
+ Transition.createTransitionAwareScrollable(FlashList);
302
284
 
303
- These components now integrate seamlessly with your transition system and provide smart gesture handling.
285
+ const TransitionLegendList =
286
+ Transition.createTransitionAwareScrollable(LegendList);
287
+ ```
304
288
 
305
- ### Configure Gesture Directions
289
+ Enable the gesture on the screen:
306
290
 
307
291
  ```tsx
308
292
  <Stack.Screen
309
- name="scrollable-screen"
310
- {...Transition.createScreenConfig({
311
- gestureDirection: ["vertical", "vertical-inverted"],
293
+ name="gallery"
294
+ options={{
312
295
  gestureEnabled: true,
313
- // ... other config
314
- })}
296
+ gestureDirection: 'vertical', // or 'horizontal', ['vertical', 'horizontal'], etc.
297
+ }}
315
298
  />
316
299
  ```
317
300
 
318
- ### Use in Your Screen
301
+ Use it in the screen:
319
302
 
320
303
  ```tsx
321
- // scrollable-screen.tsx
322
- export default function ScrollableScreen() {
304
+ export default function B() {
323
305
  return (
324
306
  <Transition.ScrollView>
325
- {/* Your scrollable content */}
307
+ {/* content */}
326
308
  </Transition.ScrollView>
327
309
  );
328
310
  }
329
311
  ```
330
312
 
331
- **Smart Gesture Handling**: The screen will only start the dismissal process when:
332
- - `vertical`: ScrollView is at the top (scrollY <= 0)
333
- - `vertical-inverted`: ScrollView is at the bottom (scrollY >= maxScroll)
334
- - `horizontal`: ScrollView is at the left edge
335
- - `horizontal-inverted`: ScrollView is at the right edge
336
-
337
- This prevents gesture conflicts and provides intuitive user interaction.
313
+ Gesture rules (handled automatically):
338
314
 
339
- ## Performance Considerations
315
+ - **vertical** – only starts when the list is at the very top
316
+ - **vertical-inverted** – only starts when the list is at the very bottom
317
+ - **horizontal** / **horizontal-inverted** – only starts when the list is at the left or right edge
340
318
 
341
- This package is designed for optimal performance, but since underlying screens remain active during transitions, following these guidelines will help maintain smooth 60fps animations:
319
+ These rules apply **only when the screen contains a nested scrollable**.
320
+ If no scroll view is present, the gesture can begin from **anywhere on the screen**—not restricted to the edges.
342
321
 
343
- ### Screen Optimization
344
- - **Keep screens lightweight**: Minimize heavy computations and complex layouts in screens that will be animated
322
+ ## Performance tips
345
323
 
346
- ### Animation Properties
347
- Prioritize transform and opacity properties over layout-affecting properties for the smoothest animations:
324
+ Keep animations smooth and responsive:
348
325
 
349
- **✅ Performant properties:**
350
- - `transform` (translateX, translateY, scale, rotate)
351
- - `opacity`
352
- - `backgroundColor` (with caution)
326
+ - **Optimize screen content** – avoid heavy computations, complex layouts, or expensive renders in screens that animate
327
+ - **Use GPU-friendly properties** – stick to `transform` and `opacity` which are hardware-accelerated; avoid animating `width`, `height`, `padding`, large `borderRadius`, and complex shadows
328
+ - **Choose appropriate timing** – use natural easing curves and durations that feel responsive without being jarring
353
329
 
354
- **❌ Avoid when possible:**
355
- - Layout properties (`width`, `height`, `padding`, `margin`)
356
- - `borderRadius` on large elements
357
- - Complex `shadowOffset` or `elevation` changes
358
- - Frequent `zIndex` modifications
330
+ ## Roadmap
359
331
 
360
- ### Easing and Timing Configuration
361
-
362
- Choose balanced easing curves to avoid perceived delays:
363
-
364
- ```tsx
365
- // ✅ Good - Smooth and responsive
366
- transitionSpec: {
367
- open: {
368
- duration: 300,
369
- easing: Easing.bezier(0.25, 0.1, 0.25, 1), // iOS-like easing
370
- },
371
- close: {
372
- duration: 250,
373
- easing: Easing.out(Easing.quad),
374
- },
375
- }
376
-
377
- // ❌ Avoid - Too snappy, may cause perceived delays
378
- transitionSpec: {
379
- close: {
380
- duration: 400,
381
- easing: Easing.bezierFn(0, 0.98, 0, 1), // Too abrupt
382
- },
383
- }
384
- ```
385
-
386
- **Why timing matters**: Screen dismissal callbacks execute when animations complete. Overly snappy configurations can create a perceived delay between gesture end and actual screen dismissal. Find the sweet spot that matches your app's personality while feeling responsive.
332
+ - **Shared element transitions** – seamless hand-off of components between screens
333
+ - **Gesture-driven forward navigation** – allow gestures to trigger push navigation events
334
+ - **Performance maximization** further reduce JS thread work and leverage Reanimated 3’s new APIs for even smoother 60 fps animations
387
335
 
388
336
 
389
337
  ## Support and Development
@@ -397,5 +345,6 @@ I apologize for any inconvenience this may cause. If you encounter issues or hav
397
345
 
398
346
 
399
347
 
348
+
400
349
  ## License
401
350
  MIT