In this tutorial, we'll create an animated collapsable card using react-native-reanimated
. We'll be starting from a provided template, which can be found at this GitHub link. The template contains an Expo project with a FlatList. Each list item has an image, title, and description. Our goal is to make the description collapsable with a smooth animation.
## Getting Started
First, clone the project from GitHub and switch to the template
branch:
git clone https://github.com/dimaportenko/reanimated-collapsable-card-tutorial.git
cd reanimated-collapsable-card-tutorial
git checkout template
Adding React Native Reanimated
We will be using the react-native-reanimated
library to create our animations. To add it, run the following command:
npx expo install react-native-reanimated
Then, you'll need to update your babel.config.js
:
module.exports = function (api) {
api.cache(true);
return {
presets: ['babel-preset-expo'],
plugins: ['react-native-reanimated/plugin'],
};
};
Implementation
In our ListItem.tsx
, we will add a new state for the height of the collapsable content:
const [height, setHeight] = useState(0);
const animatedHeight = useSharedValue(0);
We calculate the collapsible content height in the onLayout
callback:
const onLayout = (event: LayoutChangeEvent) => {
const onLayoutHeight = event.nativeEvent.layout.height;
if (onLayoutHeight > 0 && height !== onLayoutHeight) {
setHeight(onLayoutHeight);
}
};
We'll create an animated style for our collapsable content:
const collapsableStyle = useAnimatedStyle(() => {
animatedHeight.value = expanded ? withTiming(height) : withTiming(0);
return {
height: animatedHeight.value,
};
}, [expanded, height]);
We'll wrap our collapsable content in an Animated.View
:
<Animated.View style={[collapsableStyle, {overflow: 'hidden'}]}>
<View style={{ position: 'absolute' }} onLayout={onLayout}>
<Text style={[styles.details, styles.text]}>{item.details}</Text>
</View>
</Animated.View>
To make our code more maintainable, let's refactor the CollapsableContainer
into a separate reusable component:
import React, { useState } from "react";
import { LayoutChangeEvent, View, Text } from "react-native";
import Animated, {
useAnimatedStyle,
useSharedValue,
withTiming,
} from "react-native-reanimated";
export const CollapsableContainer = ({
children,
expanded,
}: {
children: React.ReactNode;
expanded: boolean;
}) => {
const [height, setHeight] = useState(0);
const animatedHeight = useSharedValue(0);
const onLayout = (event: LayoutChangeEvent) => {
const onLayoutHeight = event.nativeEvent.layout.height;
if (onLayoutHeight > 0 && height !== onLayoutHeight) {
setHeight(onLayoutHeight);
}
};
const collapsableStyle = useAnimatedStyle(() => {
animatedHeight.value = expanded ? withTiming(height) : withTiming(0);
return {
height: animatedHeight.value,
};
}, [expanded, height]);
return (
<Animated.View style={[collapsableStyle, { overflow: "hidden" }]}>
<View style={{ position: "absolute" }} onLayout={onLayout}>
{children}
</View>
</Animated.View>
);
};
Then, we can use our new CollapsableContainer
component in the ListItem
component:
export const ListItem = ({ item }: { item: ListItemType }) => {
const [expanded, setExpanded] = useState(false);
const onItemPress = () => {
setExpanded(!expanded);
};
return (
<View style={styles.wrap}>
<TouchableWithoutFeedback onPress={onItemPress}>
<View style={styles.container}>
<Image source={{ uri: item.image }} style={styles.image} />
<View style={styles.textContainer}>
<Text style={styles.text}>{item.title}</Text>
<Text style={styles.text}>{item.subtitle}</Text>
</View>
</View>
</TouchableWithoutFeedback>
<CollapsableContainer expanded={expanded}>
<Text style={[styles.details, styles.text]}>{item.details}</Text>
</CollapsableContainer>
</View>
);
};
That's it! You have successfully created an animated collapsable card in React Native using react-native-reanimated
. This animated component provides a smooth user experience, and the separate CollapsableContainer
component can be reused in different parts of your application. Happy coding! Final code
Top comments (6)
Hi,
first of all, thank you for the great tutorial!
The only problem I run into is when the "expanded" value is set to true on mount, nothing is displayed, I think it's because the
collapsableStyle
is not recomputed.How could this be resolved?
Hey,
try to add height to the dependencies like
I believe on the first render
height
is 0 and it's not triggered to recalculate on height value updated.Thanks man! I had a similar solution, but what did the trick for me was your suggestion of using { overflow: 'hidden' }, to ensure we would get the full height.
Welcome!
I dont know what I am doing wrong but this is not working for me.
if you'll share your code on github or expo snack, I can take a look.