react-native-screen-transitions 1.0.3 → 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,322 +39,312 @@ 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
38
- ```
39
-
40
- ## Usage
41
-
42
- ### Integration with expo-router
43
- In expo-router, you can define transitions in your root layout (`app/_layout.tsx`) using the `listeners` prop on `Stack.Screen`. Wrap your app in `GestureHandlerRootView` for gesture support.
44
-
45
- ```tsx
46
- // app/_layout.tsx
47
- import { Stack } from "expo-router";
48
- import { GestureHandlerRootView } from "react-native-gesture-handler";
49
- import Transition from "react-native-screen-transitions";
50
-
51
- export default function RootLayout() {
52
- return (
53
- <GestureHandlerRootView>
54
- <Stack>
55
- <Stack.Screen
56
- name="index"
57
- listeners={Transition.createConfig}
58
- />
59
- <Stack.Screen
60
- name="a"
61
- options={Transition.defaultScreenOptions()}
62
- listeners={(l) =>
63
- Transition.createConfig({
64
- ...l,
65
- ...Transition.presets.SlideFromTop(),
66
- })
67
- }
68
- />
69
- {/* Add more screens with presets */}
70
- </Stack>
71
- </GestureHandlerRootView>
72
- );
73
- }
42
+ npm install react-native-reanimated react-native-gesture-handler @react-navigation/native-stack
74
43
  ```
75
44
 
76
- **Note**: `Transition.defaultScreenOptions()` returns the required screen options for animations to work properly. It sets `presentation: "containedTransparentModal"`, `headerShown: false`, and `animation: "none"` to ensure the library can control the transition animations.
77
-
78
- For grouped routes with layouts (e.g., `app/group-a/_layout.tsx`), wrap the nested `Stack` in `Transition.View` to enable transitions:
45
+ ## Setup
79
46
 
47
+ ### 1. React Navigation
80
48
  ```tsx
81
- // app/group-a/_layout.tsx
82
- import { Stack } from "expo-router";
83
- import Transition from "react-native-screen-transitions";
84
-
85
- export default function GroupLayout() {
86
- return (
87
- <Transition.View>
88
- <Stack>
89
- <Stack.Screen name="a" />
90
- {/* Nested screens */}
91
- </Stack>
92
- </Transition.View>
93
- );
94
- }
95
- ```
96
-
97
- ### Integration with react-navigation
98
- For react-navigation, use `createNativeStackNavigator` and apply transitions via `listeners` and `options`.
99
-
100
- ```tsx
101
- // App.tsx
102
- import { createStaticNavigation } from "@react-navigation/native";
103
- import { createNativeStackNavigator } from "@react-navigation/native-stack";
104
- import { GestureHandlerRootView } from "react-native-gesture-handler";
105
- import Transition from "react-native-screen-transitions";
106
- import { Home } from "./screens/Home"; // Your screens
107
- import { ScreenA } from "./screens/ScreenA";
108
-
109
- const RootStack = createNativeStackNavigator({
110
- screens: {
111
- Home: {
112
- screen: Home,
113
- listeners: Transition.createConfig,
114
- },
115
- ScreenA: {
116
- screen: ScreenA,
117
- options: Transition.defaultScreenOptions(),
118
- listeners: (l) =>
119
- Transition.createConfig({
120
- ...l,
121
- ...Transition.presets.SlideFromTop(),
122
- }),
123
- },
124
- // Add more screens
125
- },
126
- });
49
+ import { NavigationContainer } from '@react-navigation/native';
50
+ import { GestureHandlerRootView } from 'react-native-gesture-handler';
51
+ import Transition from 'react-native-screen-transitions';
127
52
 
128
- const Navigation = createStaticNavigation(RootStack);
53
+ const Stack = Transition.createTransitionableStackNavigator();
129
54
 
130
55
  export default function App() {
131
56
  return (
132
57
  <GestureHandlerRootView>
133
- <Navigation />
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>
134
76
  </GestureHandlerRootView>
135
77
  );
136
78
  }
137
79
  ```
138
80
 
139
- For nested navigators, wrap them in `Transition.View` similar to expo-router.
140
-
141
- ### Predefined Presets
142
- Use these out-of-the-box animations via `Transition.presets`:
81
+ ### 2. Expo Router
82
+ Use the withLayoutContext to convert it into an expo router compatible navigator.
143
83
 
144
- - `SlideFromTop()`: Screen slides in from the top.
145
- - `ZoomIn()`: Screen zooms in from the center.
146
- - `SlideFromBottom()`: Screen slides in from the bottom.
147
- - `DraggableCard()`: Interactive card-like drag gesture.
148
- - `ElasticCard()`: Elastic bounce effect on drag.
149
-
150
- Example:
151
84
  ```tsx
152
- listeners={(l) => Transition.createConfig({ ...l, ...Transition.presets.DraggableCard() })}
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);
153
99
  ```
154
100
 
155
- ### Defining your own screen animations
156
- There are two ways to define custom animations: at the navigator level using `screenStyleInterpolator` (recommended for animating both screens simultaneously), or at the screen level using `useScreenAnimation` hook.
157
-
158
- #### Method 1: Navigator-Level Interpolator (Recommended)
159
- Define a `screenStyleInterpolator` at the navigator level to animate both the entering and exiting screens simultaneously. This approach provides the most control.
101
+ Use it exactly like any other Expo Router layout:
160
102
 
161
103
  ```tsx
162
- // app/_layout.tsx
163
- import { Stack } from "expo-router";
164
- import { GestureHandlerRootView } from "react-native-gesture-handler";
165
- import Transition from "react-native-screen-transitions";
166
- import { interpolate, Easing } from "react-native-reanimated";
104
+ import { Stack } from './layouts/stack.tsx'
167
105
 
168
- export default function RootLayout() {
169
- return (
106
+ export default function RootLayout(){
107
+ return(
170
108
  <GestureHandlerRootView>
171
109
  <Stack>
172
110
  <Stack.Screen
173
111
  name="a"
174
- listeners={Transition.createConfig} // Add blank config so system knows what to animate
112
+ options={{
113
+ // You usually don't want your first screen to be a transparent modal.
114
+ skipDefaultScreenOptions: true,
115
+ }}
175
116
  />
176
117
  <Stack.Screen
177
118
  name="b"
178
- options={Transition.defaultScreenOptions()}
179
- listeners={(l) =>
180
- Transition.createConfig({
181
- ...l,
182
- gestureDirection: "horizontal",
183
- gestureEnabled: true,
184
- gestureResponseDistance: 50,
185
- gestureVelocityImpact: 0.3,
186
- screenStyleInterpolator: ({
187
- current,
188
- next,
189
- layouts: { screen: { width } },
190
- }) => {
191
- "worklet";
192
-
193
- const progress = current.progress.value + (next?.progress.value || 0);
194
-
195
- const translateX = interpolate(
196
- progress,
197
- [0, 1, 2],
198
- [width, 0, width * -0.3],
199
- "clamp"
200
- );
201
-
202
- return {
203
- contentStyle: {
204
- transform: [{ translateX }],
205
- },
206
- };
207
- },
208
- transitionSpec: {
209
- open: {
210
- easing: Easing.bezier(0.25, 0.1, 0.25, 1.0),
211
- duration: 1000,
212
- },
213
- close: {
214
- damping: 10,
215
- mass: 0.5,
216
- stiffness: 100,
217
- },
218
- },
219
- })
220
- }
119
+ options={{
120
+ ...Transition.presets.SlideFromTop(),
121
+ }}
221
122
  />
222
123
  </Stack>
223
124
  </GestureHandlerRootView>
224
- );
125
+ )
225
126
  }
226
127
  ```
227
128
 
228
- When using `screenStyleInterpolator`, both screens must wrap their content in `Transition.View`:
129
+ > **Note**: `Transition.createTransitionableStackNavigator()` returns a Native Stack Navigator that's been injected with the necessary functionality for screen transitions to work.
130
+
131
+
132
+ ## Creating your screen animations
133
+
134
+ ### Using presets
135
+
136
+ Pick a built-in preset and spread it into the screen’s options.
137
+ The incoming screen automatically controls the previous screen.
229
138
 
230
139
  ```tsx
231
- // a.tsx
232
- import Transition from 'react-native-screen-transitions';
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>
161
+ ```
233
162
 
234
- export default function A() {
235
- return (
236
- <Transition.View>
237
- {/* Your content */}
238
- </Transition.View>
239
- );
240
- }
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:
241
166
 
242
- // b.tsx
243
- import Transition from 'react-native-screen-transitions';
167
+ ```tsx
168
+ // a.tsx
169
+ <Transition.View>
170
+ ...
171
+ </Transition.View>
244
172
 
245
- export default function B() {
246
- return (
247
- <Transition.View>
248
- {/* Your content */}
249
- </Transition.View>
250
- );
251
- }
173
+ // b.tsx
174
+ <Transition.View>
175
+ ...
176
+ </Transition.View>
252
177
  ```
253
178
 
254
- #### Method 2: Screen-Level Animations
255
- 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.
179
+ Without this wrapper, the transition system cannot animate the screen and the animation will appear broken or skipped.
180
+
181
+
182
+
183
+ ### Navigator-level custom animations
184
+
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.
256
187
 
257
188
  ```tsx
258
- // app/_layout.tsx
259
- <Stack.Screen
260
- name="a"
261
- listeners={Transition.createConfig}
262
- />
189
+ import { interpolate } from 'react-native-reanimated'
263
190
  <Stack.Screen
264
191
  name="b"
265
- options={Transition.defaultScreenOptions()}
266
- listeners={(l) =>
267
- Transition.createConfig({
268
- ...l,
269
- transitionSpec: {
270
- open: {
271
- easing: Easing.bezier(0.25, 0.1, 0.25, 1.0),
272
- duration: 1000,
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 }],
273
206
  },
274
- close: {
275
- damping: 10,
276
- mass: 0.5,
277
- stiffness: 100,
278
- },
279
- },
280
- })
281
- }
207
+ };
208
+ },
209
+ transitionSpec: {
210
+ close: Transition.specs.DefaultSpec,
211
+ open: Transition.specs.DefaultSpec,
212
+ },
213
+ }}
282
214
  />
283
215
  ```
284
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
+
285
223
  ```tsx
286
- // a.tsx (previous screen)
287
224
  import { useScreenAnimation } from 'react-native-screen-transitions';
288
225
  import Animated, { useAnimatedStyle, interpolate } from 'react-native-reanimated';
289
226
 
290
- export default function A() {
291
- const { next, layouts: { screen: { width } } } = useScreenAnimation();
227
+ export default function BScreen() {
228
+ const { current } = useScreenAnimation();
292
229
 
293
230
  const animatedStyle = useAnimatedStyle(() => {
294
- // Unfocusing animation - screen slides left when next screen enters
295
- const translateX = interpolate(next?.progress.value || 0, [0, 1], [0, width * -0.3]);
296
231
  return {
297
- transform: [{ translateX }],
232
+ opacity: current.progress.value
298
233
  };
299
234
  });
300
235
 
301
236
  return (
302
- <Animated.View style={animatedStyle}>
237
+ <Animated.View style={[{ flex: 1 }, animatedStyle]}>
303
238
  {/* Your content */}
304
239
  </Animated.View>
305
240
  );
306
241
  }
242
+ ```
307
243
 
308
- // b.tsx (entering screen)
309
- import { useScreenAnimation } from 'react-native-screen-transitions';
310
- import Animated, { useAnimatedStyle, interpolate } from 'react-native-reanimated';
244
+ ## Apply screen transitions to nested navigators
311
245
 
312
- export default function B() {
313
- 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`.
314
247
 
315
- const animatedStyle = useAnimatedStyle(() => {
316
- // Focusing animation - screen slides in from right
317
- const translateX = interpolate(current.progress.value, [0, 1], [width, 0]);
318
- return {
319
- transform: [{ translateX }],
320
- };
321
- });
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';
322
252
 
253
+ export default function BLayout() {
323
254
  return (
324
- <Animated.View style={[{ flex: 1 }, animatedStyle]}>
325
- {/* Your content */}
326
- </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>
327
261
  );
328
262
  }
263
+ ```
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.
266
+
267
+ ## Swipe-to-dismiss with scrollables
268
+
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:
271
+
272
+ ```tsx
273
+ import Transition from 'react-native-screen-transitions';
274
+ import { LegendList } from "@legendapp/list"
275
+ import { FlashList } from "@shopify/flash-list";
276
+
277
+ // Drop-in replacements
278
+ const ScrollView = Transition.ScrollView;
279
+ const FlatList = Transition.FlatList;
329
280
 
281
+ // Or wrap any list you like
282
+ const TransitionFlashList =
283
+ Transition.createTransitionAwareScrollable(FlashList);
284
+
285
+ const TransitionLegendList =
286
+ Transition.createTransitionAwareScrollable(LegendList);
287
+ ```
330
288
 
289
+ Enable the gesture on the screen:
290
+
291
+ ```tsx
292
+ <Stack.Screen
293
+ name="gallery"
294
+ options={{
295
+ gestureEnabled: true,
296
+ gestureDirection: 'vertical', // or 'horizontal', ['vertical', 'horizontal'], etc.
297
+ }}
298
+ />
331
299
  ```
332
300
 
301
+ Use it in the screen:
302
+
303
+ ```tsx
304
+ export default function B() {
305
+ return (
306
+ <Transition.ScrollView>
307
+ {/* content */}
308
+ </Transition.ScrollView>
309
+ );
310
+ }
311
+ ```
312
+
313
+ Gesture rules (handled automatically):
314
+
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
318
+
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.
321
+
322
+ ## Performance tips
323
+
324
+ Keep animations smooth and responsive:
325
+
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
329
+
330
+ ## Roadmap
331
+
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
335
+
336
+
337
+ ## Support and Development
333
338
 
334
- ### Known Issues and Roadmap
335
- This is a WIP package, so expect improvements. Current known issues:
336
- - **Gestures with ScrollViews**: Gestures can be wonky when combined with scrollable content. For example, if a screen defines vertical dismissal gestures and contains a vertical `ScrollView`, the gesture may not trigger reliably (conflicts with scroll handling).
337
- - **Gestures Dismisall with Nested Navigators**: When using nested navigators with gesture dismissal enabled, dismissing a nested screen via gesture may cause the transparent modal to appear dismissed while remaining open. This affects the visual state but not the actual navigation state.
338
- - **Web Support**: Not intended or tested for web—focus is on mobile (iOS/Android). Web may have issues with gestures and animations.
339
+ This package is provided as-is and is developed in my free time. While I strive to maintain and improve it, please understand that:
339
340
 
341
+ - **Updates and bug fixes** may take time to implement
342
+ - **Feature requests** will be considered but may not be prioritized immediately
340
343
 
341
- ### Note:
344
+ I apologize for any inconvenience this may cause. If you encounter issues or have suggestions, please feel free to open an issue on the repository.
342
345
 
343
- This package is something I'll work on in my freetime. However, you can see the roadmap I plan on following in the coming future.
344
346
 
345
- Roadmap:
346
- - Fix gesture conflicts with ScrollViews (e.g., better gesture priority handling).
347
- - Add more presets and customization options.
348
- - Improve documentation and examples.
349
- - Potential web support if demand arises.
350
- - Testing for more edge cases (e.g., modals, tabs).
351
347
 
352
- See the examples in `/examples/expo-router-example` and `/examples/react-nav-example` for full demos.
353
348
 
354
349
  ## License
355
350
  MIT