├── .nvmrc
├── .gitignore
├── .npmignore
├── src
├── index.ts
└── Push.tsx
├── example
├── showcase.gif
├── assets
│ ├── icon.png
│ ├── favicon.png
│ ├── splash-icon.png
│ └── adaptive-icon.png
├── tsconfig.json
├── index.ts
├── .gitignore
├── package.json
├── app.json
└── App.tsx
├── tsconfig.json
├── .github
└── workflows
│ ├── build.yml
│ └── publish.yml
├── LICENSE
├── package.json
├── README.md
└── yarn.lock
/.nvmrc:
--------------------------------------------------------------------------------
1 | v20.11.1
2 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | /node_modules
2 | /dist
--------------------------------------------------------------------------------
/.npmignore:
--------------------------------------------------------------------------------
1 | /node_modules
2 | /src
3 | /showcase
4 | /tsconfig.json
--------------------------------------------------------------------------------
/src/index.ts:
--------------------------------------------------------------------------------
1 | export { WithPushTransition, WithPushTransitionProps } from "./Push";
2 |
--------------------------------------------------------------------------------
/example/showcase.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/TheLartians/react-native-simple-transition/HEAD/example/showcase.gif
--------------------------------------------------------------------------------
/example/assets/icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/TheLartians/react-native-simple-transition/HEAD/example/assets/icon.png
--------------------------------------------------------------------------------
/example/assets/favicon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/TheLartians/react-native-simple-transition/HEAD/example/assets/favicon.png
--------------------------------------------------------------------------------
/example/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "expo/tsconfig.base",
3 | "compilerOptions": {
4 | "strict": true
5 | }
6 | }
7 |
--------------------------------------------------------------------------------
/example/assets/splash-icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/TheLartians/react-native-simple-transition/HEAD/example/assets/splash-icon.png
--------------------------------------------------------------------------------
/example/assets/adaptive-icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/TheLartians/react-native-simple-transition/HEAD/example/assets/adaptive-icon.png
--------------------------------------------------------------------------------
/example/index.ts:
--------------------------------------------------------------------------------
1 | import { registerRootComponent } from 'expo';
2 |
3 | import App from './App';
4 |
5 | // registerRootComponent calls AppRegistry.registerComponent('main', () => App);
6 | // It also ensures that whether you load the app in Expo Go or in a native build,
7 | // the environment is set up appropriately
8 | registerRootComponent(App);
9 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "module": "commonjs",
4 | "target": "esnext",
5 | "lib": ["es6", "es2019"],
6 | "esModuleInterop": true,
7 | "strict": true,
8 | "pretty": true,
9 | "moduleResolution": "node",
10 | "sourceMap": true,
11 | "outDir": "dist",
12 | "declaration": true,
13 | "jsx": "react",
14 | },
15 | "include": ["src"],
16 | "exclude": ["node_modules", "dist"],
17 | "lib": ["es2015"]
18 | }
--------------------------------------------------------------------------------
/.github/workflows/build.yml:
--------------------------------------------------------------------------------
1 | name: Build
2 |
3 | on:
4 | push:
5 | branches:
6 | - master
7 | pull_request:
8 | branches:
9 | - master
10 |
11 | jobs:
12 | build:
13 | runs-on: ubuntu-latest
14 | steps:
15 | - uses: actions/checkout@v1
16 |
17 | - name: install
18 | run: yarn install
19 |
20 | - name: build
21 | run: yarn build
22 |
23 | - name: check style
24 | run: yarn check:style
25 |
--------------------------------------------------------------------------------
/example/.gitignore:
--------------------------------------------------------------------------------
1 | # Learn more https://docs.github.com/en/get-started/getting-started-with-git/ignoring-files
2 |
3 | # dependencies
4 | node_modules/
5 |
6 | # Expo
7 | .expo/
8 | dist/
9 | web-build/
10 | expo-env.d.ts
11 |
12 | # Native
13 | .kotlin/
14 | *.orig.*
15 | *.jks
16 | *.p8
17 | *.p12
18 | *.key
19 | *.mobileprovision
20 |
21 | # Metro
22 | .metro-health-check*
23 |
24 | # debug
25 | npm-debug.*
26 | yarn-debug.*
27 | yarn-error.*
28 |
29 | # macOS
30 | .DS_Store
31 | *.pem
32 |
33 | # local env files
34 | .env*.local
35 |
36 | # typescript
37 | *.tsbuildinfo
38 |
--------------------------------------------------------------------------------
/example/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "example",
3 | "version": "1.0.0",
4 | "main": "index.ts",
5 | "scripts": {
6 | "start": "expo start",
7 | "android": "expo start --android",
8 | "ios": "expo start --ios",
9 | "web": "expo start --web"
10 | },
11 | "dependencies": {
12 | "expo": "~53.0.20",
13 | "expo-status-bar": "~2.2.3",
14 | "react": "19.0.0",
15 | "react-native": "0.79.5",
16 | "react-native-simple-transition": "../"
17 | },
18 | "devDependencies": {
19 | "@babel/core": "^7.25.2",
20 | "@types/react": "~19.0.10",
21 | "typescript": "~5.8.3"
22 | },
23 | "private": true
24 | }
25 |
--------------------------------------------------------------------------------
/.github/workflows/publish.yml:
--------------------------------------------------------------------------------
1 | # Publish new commits to npm. See https://github.com/mikeal/merge-release/blob/master/README.md for more info.
2 |
3 | name: Publish
4 |
5 | on:
6 | push:
7 | branches:
8 | - master
9 |
10 | jobs:
11 | build:
12 | runs-on: ubuntu-latest
13 | steps:
14 | - uses: actions/checkout@v1
15 |
16 | - name: install
17 | run: yarn install
18 |
19 | - name: build
20 | run: yarn build
21 |
22 | - name: Publish
23 | if: github.ref == 'refs/heads/master'
24 | uses: Github-Actions-Community/merge-release@main
25 | env:
26 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
27 | NPM_AUTH_TOKEN: ${{ secrets.NPM_AUTH_TOKEN }}
28 |
--------------------------------------------------------------------------------
/example/app.json:
--------------------------------------------------------------------------------
1 | {
2 | "expo": {
3 | "name": "example",
4 | "slug": "example",
5 | "version": "1.0.0",
6 | "orientation": "portrait",
7 | "icon": "./assets/icon.png",
8 | "userInterfaceStyle": "light",
9 | "newArchEnabled": true,
10 | "splash": {
11 | "image": "./assets/splash-icon.png",
12 | "resizeMode": "contain",
13 | "backgroundColor": "#ffffff"
14 | },
15 | "ios": {
16 | "supportsTablet": true
17 | },
18 | "android": {
19 | "adaptiveIcon": {
20 | "foregroundImage": "./assets/adaptive-icon.png",
21 | "backgroundColor": "#ffffff"
22 | },
23 | "edgeToEdgeEnabled": true
24 | },
25 | "web": {
26 | "favicon": "./assets/favicon.png"
27 | }
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2020 Lars Melchior
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "react-native-simple-transition",
3 | "version": "0.1.0",
4 | "description": "A small component that automatically transitions child changes",
5 | "author": "Lars Melchior",
6 | "license": "MIT",
7 | "types": "dist/index.d.ts",
8 | "repository": {
9 | "type": "git",
10 | "url": "https://github.com/TheLartians/react-native-simple-transition"
11 | },
12 | "files": [
13 | "dist"
14 | ],
15 | "main": "dist/index.js",
16 | "devDependencies": {
17 | "@types/react-native": "^0.63.8",
18 | "prettier": "^2.0.5",
19 | "typescript": "^3.9.7"
20 | },
21 | "scripts": {
22 | "build": "tsc",
23 | "watch": "tsc --watch",
24 | "prepublish": "npm run build",
25 | "check:style": "prettier --check \"src/**/*[!.d].ts\" \"src/**/*.tsx\"",
26 | "fix:style": "prettier --check \"src/**/*.ts\" \"src/**/*.tsx\" --write",
27 | "example": "yarn --cwd example"
28 | },
29 | "keywords": [
30 | "android",
31 | "animation",
32 | "fade",
33 | "ios",
34 | "mobile",
35 | "react-native",
36 | "react-native-component",
37 | "transition",
38 | "ui"
39 | ]
40 | }
41 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | 
2 | [](https://badge.fury.io/js/react-native-simple-transition)
3 |
4 | # react-native-simple-transition
5 |
6 | A minimalist and easy to use transition component for React Native.
7 |
8 | 
9 |
10 | ## Usage
11 |
12 | Install the library
13 |
14 | ```bash
15 | yarn add react-native-simple-transition
16 | ```
17 |
18 | Create a transition component and add the content as a child.
19 | New components will transition with an animation every time the content key changes.
20 |
21 | ```tsx
22 | import { WithPushTransition } from 'react-native-simple-transition';
23 |
24 | const MyComponent = () => {
25 | const [count, setCount] = useState(0);
26 |
27 | return (
28 |
29 | setCount(count+1)}>
30 | This component will smoothly transition on key changes.
31 |
32 |
33 | )
34 | }
35 | ```
36 |
37 | Currently the only transition component is `WithPushTransition`.
38 | More are planned to be added soon.
39 |
40 | ## WithPushTransition
41 |
42 | ### Optional properties
43 |
44 | - `contentKey`: alternative to updating the child's `key` property
45 | - `duration`: transition duration in milliseconds
46 | - `style`: the style given to the transition component
47 | - `easing`: an [easing function](https://reactnative.dev/docs/easing) for the transition
48 | - `direction`: the direction of the transition; can be `"left"`, `"right"`, `"up"` or `"down"`
49 |
--------------------------------------------------------------------------------
/yarn.lock:
--------------------------------------------------------------------------------
1 | # THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
2 | # yarn lockfile v1
3 |
4 |
5 | "@types/prop-types@*":
6 | version "15.7.3"
7 | resolved "https://registry.yarnpkg.com/@types/prop-types/-/prop-types-15.7.3.tgz#2ab0d5da2e5815f94b0b9d4b95d1e5f243ab2ca7"
8 | integrity sha512-KfRL3PuHmqQLOG+2tGpRO26Ctg+Cq1E01D2DMriKEATHgWLfeNDmq9e29Q9WIky0dQ3NPkd1mzYH8Lm936Z9qw==
9 |
10 | "@types/react-native@^0.63.8":
11 | version "0.63.8"
12 | resolved "https://registry.yarnpkg.com/@types/react-native/-/react-native-0.63.8.tgz#73ec087122c64c309eeaf150b565b8d755f0fb1f"
13 | integrity sha512-QRwGFRTyGafRVTUS+0GYyJrlpmS3boyBaFI0ULSc+mh/lQNxrzbdQvoL2k5X7+t9hxyqA4dTQAlP6l0ir/fNJQ==
14 | dependencies:
15 | "@types/react" "*"
16 |
17 | "@types/react@*":
18 | version "16.9.46"
19 | resolved "https://registry.yarnpkg.com/@types/react/-/react-16.9.46.tgz#f0326cd7adceda74148baa9bff6e918632f5069e"
20 | integrity sha512-dbHzO3aAq1lB3jRQuNpuZ/mnu+CdD3H0WVaaBQA8LTT3S33xhVBUj232T8M3tAhSWJs/D/UqORYUlJNl/8VQZg==
21 | dependencies:
22 | "@types/prop-types" "*"
23 | csstype "^3.0.2"
24 |
25 | csstype@^3.0.2:
26 | version "3.0.2"
27 | resolved "https://registry.yarnpkg.com/csstype/-/csstype-3.0.2.tgz#ee5ff8f208c8cd613b389f7b222c9801ca62b3f7"
28 | integrity sha512-ofovWglpqoqbfLNOTBNZLSbMuGrblAf1efvvArGKOZMBrIoJeu5UsAipQolkijtyQx5MtAzT/J9IHj/CEY1mJw==
29 |
30 | prettier@^2.0.5:
31 | version "2.0.5"
32 | resolved "https://registry.yarnpkg.com/prettier/-/prettier-2.0.5.tgz#d6d56282455243f2f92cc1716692c08aa31522d4"
33 | integrity sha512-7PtVymN48hGcO4fGjybyBSIWDsLU4H4XlvOHfq91pz9kkGlonzwTfYkaIEwiRg/dAJF9YlbsduBAgtYLi+8cFg==
34 |
35 | typescript@^3.9.7:
36 | version "3.9.7"
37 | resolved "https://registry.yarnpkg.com/typescript/-/typescript-3.9.7.tgz#98d600a5ebdc38f40cb277522f12dc800e9e25fa"
38 | integrity sha512-BLbiRkiBzAwsjut4x/dsibSTB6yWpwT5qWmC2OfuCg3GgVQCSgMs4vEctYPhsaGtd0AeuuHMkjZ2h2WG8MSzRw==
39 |
--------------------------------------------------------------------------------
/example/App.tsx:
--------------------------------------------------------------------------------
1 | import React, {useState} from 'react';
2 | import {
3 | SafeAreaView,
4 | StyleSheet,
5 | Text,
6 | TouchableOpacity,
7 | View,
8 | } from 'react-native';
9 | import {WithPushTransition} from 'react-native-simple-transition';
10 |
11 | const App = () => {
12 | const [count, setCount] = useState(0);
13 | const direction = (['up', 'down', 'left', 'right'] as const)[count % 4];
14 |
15 | return (
16 |
17 |
22 |
27 | {count}: {direction}
28 |
29 |
30 | setCount(count + 1)}>
33 | Next
34 |
35 |
36 | );
37 | };
38 |
39 | const styles = StyleSheet.create({
40 | appContainer: {
41 | flex: 1,
42 | justifyContent: 'center',
43 | backgroundColor: 'white',
44 | },
45 | buttonContainer: {
46 | backgroundColor: 'blue',
47 | paddingVertical: 10,
48 | paddingHorizontal: 50,
49 | borderRadius: 100,
50 | justifyContent: 'center',
51 | marginTop: 0,
52 | margin: 10,
53 | },
54 | buttonLabel: {
55 | color: 'white',
56 | fontWeight: 'bold',
57 | fontSize: 20,
58 | textAlign: 'center',
59 | },
60 | innerWindow: {
61 | flex: 1,
62 | borderRadius: 20,
63 | overflow: 'hidden',
64 | margin: 10,
65 | },
66 | innerContainer: {
67 | width: '100%',
68 | height: '100%',
69 | alignItems: 'center',
70 | justifyContent: 'center',
71 | },
72 | innerLabel: {color: 'white', fontSize: 20, fontWeight: 'bold'},
73 | });
74 |
75 | export default App;
76 |
--------------------------------------------------------------------------------
/src/Push.tsx:
--------------------------------------------------------------------------------
1 | import React, { useState, useEffect, useMemo, ReactNode } from "react";
2 | import {
3 | Animated,
4 | View,
5 | Easing,
6 | ViewStyle,
7 | Text,
8 | EasingFunction,
9 | } from "react-native";
10 |
11 | export type WithPushTransitionProps = {
12 | children: React.ReactNode;
13 | /** A unique key for the currently displayed content. When changing content update this key to trigger a transition. */
14 | contentKey: string | number;
15 | /** The animation duration. */
16 | duration?: number;
17 | /** The style for the container View of the content */
18 | style?: ViewStyle;
19 | /** The easing function for the transition animation. */
20 | easing?: EasingFunction;
21 | /** The direction for the push animation. */
22 | direction?: "left" | "right" | "up" | "down";
23 | };
24 |
25 | function createTransform(
26 | value: Animated.Value,
27 | idx: number,
28 | width: number,
29 | height: number,
30 | direction: WithPushTransitionProps["direction"]
31 | ) {
32 | switch (direction) {
33 | case "right":
34 | return {
35 | translateX: value.interpolate({
36 | inputRange: [0, 1],
37 | outputRange: [width * (idx - 1), width * idx],
38 | }),
39 | };
40 | default:
41 | case "left":
42 | return {
43 | translateX: value.interpolate({
44 | inputRange: [0, 1],
45 | outputRange: [width * (1 - idx), -width * idx],
46 | }),
47 | };
48 | case "up":
49 | return {
50 | translateY: value.interpolate({
51 | inputRange: [0, 1],
52 | outputRange: [height * (1 - idx), -height * idx],
53 | }),
54 | };
55 | case "down":
56 | return {
57 | translateY: value.interpolate({
58 | inputRange: [0, 1],
59 | outputRange: [height * (idx - 1), height * idx],
60 | }),
61 | };
62 | }
63 | }
64 |
65 | export const WithPushTransition = ({
66 | contentKey,
67 | children,
68 | duration,
69 | easing,
70 | style,
71 | direction = "left",
72 | }: WithPushTransitionProps): ReactNode => {
73 | const [width, setWidth] = useState(0);
74 | const [height, setHeight] = useState(0);
75 |
76 | const [current, setCurrent] = useState(children);
77 | const [currentKey, setCurrentKey] = useState(contentKey);
78 | const [previous, setPrevious] = useState();
79 |
80 | const animatedValue = useMemo(() => new Animated.Value(1), []);
81 |
82 | const animation = useMemo(
83 | () =>
84 | Animated.timing(animatedValue, {
85 | toValue: 1,
86 | duration: duration ?? 1000,
87 | easing: easing ?? Easing.inOut(Easing.ease),
88 | useNativeDriver: true,
89 | }),
90 | [animatedValue, duration, easing]
91 | );
92 |
93 | useEffect(() => {
94 | if (currentKey !== contentKey) {
95 | // add the new children to the current views
96 | setPrevious(current);
97 | setCurrentKey(contentKey);
98 | setCurrent(children);
99 |
100 | animation.stop();
101 | requestAnimationFrame(() => {
102 | animatedValue.setValue(0);
103 | requestAnimationFrame(() => {
104 | animation.start(() => {
105 | // finish the animation by removing the previous children.
106 | setPrevious(undefined);
107 | });
108 | });
109 | });
110 | } else {
111 | // the key is identical so current view component needs to be updated
112 | // the animation should continue running
113 | setCurrent(children);
114 | }
115 | }, [currentKey, children]);
116 |
117 | return (
118 | {
121 | setWidth(event.nativeEvent.layout.width);
122 | setHeight(event.nativeEvent.layout.height);
123 | }}
124 | >
125 | {previous && (
126 |
136 | {previous}
137 |
138 | )}
139 |
149 | {current}
150 |
151 |
152 | );
153 | };
154 |
--------------------------------------------------------------------------------