logoChisa UI

Command Palette

Search for a command to run...

Componentschevron_rightTransitionPanel

TransitionPanel

A motion component that switches content with custom enter/exit animations.

9:41
signal_cellular_altwifibattery_full
Transition Panel Preview

Installation

$

Usage

Example.tsx
import { TransitionPanel } from './components/TransitionPanel';
export default function App() {
const [index, setIndex] = React.useState(0);
return (
<TransitionPanel
activeIndex={index}
variants={{
enter: { opacity: 0, y: 50 },
center: { opacity: 1, y: 0 },
exit: { opacity: 0, y: -50 }
}}
>
<View style={{ backgroundColor: 'red', height: 100 }} />
<View style={{ backgroundColor: 'blue', height: 100 }} />
</TransitionPanel>
);
}

Full Component Code

TransitionPanel.tsx
1import React from 'react';
2import { StyleSheet, View, ViewStyle } from 'react-native';
3import Animated, {
4 Keyframe,
5 LinearTransition,
6 Easing,
7 AnimatedProps
8} from 'react-native-reanimated';
9
10export type TransitionSpec = {
11 opacity?: number;
12 x?: number;
13 y?: number;
14 scale?: number;
15 rotate?: string;
16};
17
18export type TransitionPanelProps = AnimatedProps<ViewStyle> & {
19 children: React.ReactNode[];
20 activeIndex: number;
21 transition?: {
22 duration?: number;
23 easing?: (value: number) => number;
24 };
25 variants?: {
26 enter: TransitionSpec;
27 center: TransitionSpec;
28 exit: TransitionSpec;
29 };
30 style?: ViewStyle;
31};
32
33const DEFAULT_VARIANTS = {
34 enter: { opacity: 0, scale: 0.9 },
35 center: { opacity: 1, scale: 1 },
36 exit: { opacity: 0, scale: 0.9 },
37};
38
39export function TransitionPanel({
40 children,
41 activeIndex,
42 transition = { duration: 300 },
43 variants = DEFAULT_VARIANTS,
44 style,
45 ...rest
46}: TransitionPanelProps) {
47 if (activeIndex < 0 || activeIndex >= React.Children.count(children)) {
48 return null;
49 }
50
51 // Convert separate x/y props to transform array for Reanimated
52 const buildTransform = (spec: TransitionSpec) => {
53 const transform: any[] = [];
54 if (spec.x !== undefined) transform.push({ translateX: spec.x });
55 if (spec.y !== undefined) transform.push({ translateY: spec.y });
56 if (spec.scale !== undefined) transform.push({ scale: spec.scale });
57 if (spec.rotate !== undefined) transform.push({ rotate: spec.rotate });
58 return transform;
59 };
60
61 const EnteringAnimation = new Keyframe({
62 0: {
63 opacity: variants.enter.opacity ?? 0,
64 transform: buildTransform(variants.enter),
65 },
66 100: {
67 opacity: variants.center.opacity ?? 1,
68 transform: buildTransform(variants.center),
69 easing: Easing.bezier(0.25, 0.1, 0.25, 1),
70 },
71 }).duration(transition.duration ?? 250);
72
73 const ExitingAnimation = new Keyframe({
74 0: {
75 opacity: variants.center.opacity ?? 1,
76 transform: buildTransform(variants.center),
77 },
78 100: {
79 opacity: variants.exit.opacity ?? 0,
80 transform: buildTransform(variants.exit),
81 easing: Easing.bezier(0.25, 0.1, 0.25, 1),
82 },
83 }).duration(transition.duration ?? 250);
84
85 return (
86 <View style={[styles.container, style]} {...rest}>
87 <Animated.View
88 key={activeIndex}
89 entering={EnteringAnimation}
90 exiting={ExitingAnimation}
91 layout={LinearTransition.springify().damping(20).stiffness(200)}
92 style={styles.animatedChild}
93 >
94 {children[activeIndex]}
95 </Animated.View>
96 </View>
97 );
98}
99
100const styles = StyleSheet.create({
101 container: {
102 width: '100%',
103 overflow: 'hidden',
104 },
105 animatedChild: {
106 width: '100%',
107 },
108});

Props

NameTypeDefaultDescription
childrenReact.ReactNode[]-Array of content nodes to switch between
activeIndexnumber0Index of the currently active child
variantsobject{ enter, center, exit }Animation variants for enter/center/exit states
transitionobject{ duration: 300 }Transition configuration