Entering, Exiting and Shared Elements
Reanimated's entering, exiting and layout props, and how to build a shared element transition that Reanimated 4 does not provide.
<Animated.View entering={FadeInDown.duration(200)} exiting={FadeOut} layout={LinearTransition} />Reanimated can animate an element as it joins the tree, as it leaves, and as its position changes. Those are the entering, exiting and layout props on a Reanimated Animated component. They are not CSS transitions: nothing is declared in a stylesheet, and nothing inherits to children.
Entering and exiting
Pass a preset — FadeIn, FadeInDown, SlideInRight, ZoomIn, BounceOut and the rest. Modifiers chain off them: .duration(200), .delay(40), .easing(), .springify(), .withInitialValues(), .withCallback() and .reduceMotion(). Define them outside the component or in useMemo so a render does not rebuild them. To stop a whole list animating in on first mount, wrap it in <LayoutAnimationConfig skipEntering>, which also takes skipExiting.
Layout transitions
The layout prop animates a view to its new position and size when layout changes: LinearTransition, SequencedTransition, FadingTransition, CurvedTransition, JumpingTransition and EntryExitTransition. Removing one row and letting the others close the gap is the usual case.
Shared elements
Reanimated 4.5 documents no shared element transition, so a thumbnail that grows into a detail image is something you build. Measure the source rectangle — measureInWindow on a View ref, or Reanimated's measure with useAnimatedRef, which returns x, y, pageX, pageY, width, height or null — then animate an absolutely positioned overlay from that rectangle to the destination geometry. A cross-fade between the two screens is often enough and costs nothing to maintain.
Example
import { useState } from 'react';
import { Pressable, StyleSheet, Text, View } from 'react-native';
import Animated, { FadeInDown, FadeOut, LinearTransition } from 'react-native-reanimated';
type Task = { id: string; label: string };
const initialTasks: Task[] = [
{ id: 'a', label: 'Buy milk' },
{ id: 'b', label: 'Call the bank' },
{ id: 'c', label: 'Book flights' },
];
export default function TaskList() {
const [tasks, setTasks] = useState<Task[]>(initialTasks);
const remove = (id: string) => {
setTasks((current) => current.filter((task) => task.id !== id));
};
return (
<View style={styles.screen}>
{tasks.map((task) => (
<Animated.View
key={task.id} // stable key: with an index the wrong row would animate
entering={FadeInDown.duration(200)}
exiting={FadeOut.duration(150)}
layout={LinearTransition.duration(200)}
style={styles.row}>
<Text>{task.label}</Text>
<Pressable onPress={() => remove(task.id)}>
<Text style={styles.remove}>Remove</Text>
</Pressable>
</Animated.View>
))}
</View>
);
}
const styles = StyleSheet.create({
screen: { flex: 1, padding: 16, gap: 8 },
row: {
flexDirection: 'row', // flexDirection is column by default here
justifyContent: 'space-between',
padding: 14,
borderRadius: 10,
backgroundColor: '#ffffff',
},
remove: { color: '#c62828', fontWeight: '600' },
});When to use it
- Remove a task and have the rows below slide up instead of jumping.
- Stagger list rows in as fetched data arrives.
- Grow a tapped thumbnail into a full-width image.
- Suppress entrance animations for cached data while keeping them for new items.
More examples
Skip the entrance on first mount
On screen: the two stored notes are simply there when the screen opens, and each new note fades down into place while the others slide to make room. Without LayoutAnimationConfig the whole list performs its entrance every time the screen mounts, which reads as slowness rather than polish. Use skipEntering for cached or already-known data, and let only genuinely new items animate.
import { useState } from 'react';
import { Pressable, StyleSheet, Text, View } from 'react-native';
import Animated, {
FadeInDown,
FadeOut,
LayoutAnimationConfig,
LinearTransition,
} from 'react-native-reanimated';
type Note = { id: string; text: string };
const saved: Note[] = [
{ id: '1', text: 'Renew passport' },
{ id: '2', text: 'Send the invoice' },
];
export default function Notes() {
const [notes, setNotes] = useState<Note[]>(saved);
const add = () => {
setNotes((current) => [
{ id: String(current.length + 1), text: `Note ${current.length + 1}` },
...current,
]);
};
return (
<View style={styles.screen}>
<Pressable onPress={add}>
<Text style={styles.add}>Add note</Text>
</Pressable>
{/* already-saved notes appear instantly; notes added later animate in */}
<LayoutAnimationConfig skipEntering>
{notes.map((note) => (
<Animated.View
key={note.id}
entering={FadeInDown.duration(180)}
exiting={FadeOut.duration(120)}
layout={LinearTransition.duration(180)}
style={styles.row}>
<Text>{note.text}</Text>
</Animated.View>
))}
</LayoutAnimationConfig>
</View>
);
}
const styles = StyleSheet.create({
screen: { flex: 1, padding: 16, gap: 8 },
add: { fontWeight: '600', color: '#1976d2' },
row: { padding: 14, borderRadius: 10, backgroundColor: '#eceff1' },
});A shared element you build yourself
On screen: tapping the small thumbnail makes a copy of the image grow from exactly where the thumbnail sits to a full-width hero 120 points down the screen, over 280ms. This is the honest substitute for a shared element transition, since Reanimated 4 ships none: measure the source, interpolate the four geometry values by hand, and keep the overlay out of the touch path with pointerEvents. collapsable={false} keeps Android from optimising the measured view away.
import { useRef, useState } from 'react';
import { Image, Pressable, StyleSheet, View, useWindowDimensions } from 'react-native';
import Animated, { useAnimatedStyle, useSharedValue, withTiming } from 'react-native-reanimated';
type Rect = { x: number; y: number; width: number; height: number };
export default function ThumbnailToHero() {
const thumb = useRef<View>(null);
const { width } = useWindowDimensions();
const [from, setFrom] = useState<Rect | null>(null);
const progress = useSharedValue(0);
const open = () => {
// measureInWindow reports window coordinates, which is what an overlay needs
thumb.current?.measureInWindow((x, y, w, h) => {
setFrom({ x, y, width: w, height: h });
progress.value = withTiming(1, { duration: 280 });
});
};
const overlayStyle = useAnimatedStyle(() => {
const start = from ?? { x: 0, y: 0, width: 0, height: 0 };
const p = progress.value;
return {
opacity: from === null ? 0 : 1,
left: start.x * (1 - p),
top: start.y + (120 - start.y) * p,
width: start.width + (width - start.width) * p,
height: start.height + (width * 0.6 - start.height) * p,
};
});
return (
<View style={styles.screen}>
<Pressable onPress={open}>
<View ref={thumb} collapsable={false} style={styles.thumb}>
<Image source={require('./harbour.jpg')} style={styles.fill} />
</View>
</Pressable>
<Animated.View pointerEvents="none" style={[styles.overlay, overlayStyle]}>
<Image source={require('./harbour.jpg')} style={styles.fill} />
</Animated.View>
</View>
);
}
const styles = StyleSheet.create({
screen: { flex: 1, padding: 16 },
thumb: { width: 80, height: 80, borderRadius: 8, overflow: 'hidden' },
overlay: { position: 'absolute' },
fill: { width: '100%', height: '100%' },
});Staggering rows into a list
On screen: rows arrive one after another from slightly below, each 40ms behind the one above, landing with a light spring. The delay is computed from the renderItem index, so the stagger follows list order rather than arrival order. Keep the per-row delay small: with twenty rows at 100ms each, the last one arrives two seconds after the first and the screen feels broken.
import { FlatList, StyleSheet, Text } from 'react-native';
import Animated, { FadeInDown } from 'react-native-reanimated';
type Photo = { id: string; caption: string };
const photos: Photo[] = [
{ id: 'a', caption: 'Harbour at dawn' },
{ id: 'b', caption: 'Ferry terminal' },
{ id: 'c', caption: 'Bridge at night' },
];
export default function Gallery() {
return (
<FlatList
data={photos}
keyExtractor={(item) => item.id}
contentContainerStyle={styles.list}
renderItem={({ item, index }) => (
// 40 ms per row is a stagger; much more becomes a queue to wait for
<Animated.View
entering={FadeInDown.delay(index * 40).springify()}
style={styles.card}>
<Text>{item.caption}</Text>
</Animated.View>
)}
/>
);
}
const styles = StyleSheet.create({
list: { padding: 16, gap: 8 },
card: { padding: 16, borderRadius: 12, backgroundColor: '#ffffff' },
});
Discussion