DEV Community

Cover image for Collapsible Card with React Native Reanimated
Dima Portenko
Dima Portenko

Posted on • Updated on

Collapsible Card with React Native Reanimated

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.

Demo gif

## 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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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'],
  };
};
Enter fullscreen mode Exit fullscreen mode

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);
Enter fullscreen mode Exit fullscreen mode

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);
  }
};
Enter fullscreen mode Exit fullscreen mode

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]);
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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>
  );
};
Enter fullscreen mode Exit fullscreen mode

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>
  );
};
Enter fullscreen mode Exit fullscreen mode

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)

Collapse
 
skizzo profile image
Stephan Müller

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?

Collapse
 
dimaportenko profile image
Dima Portenko

Hey,
try to add height to the dependencies like

 const collapsableStyle = useAnimatedStyle(() => {
    animatedHeight.value = expanded ? withTiming(height) : withTiming(0);

    return {
      height: animatedHeight.value,
    };
  }, [expanded, height]);
Enter fullscreen mode Exit fullscreen mode

I believe on the first render height is 0 and it's not triggered to recalculate on height value updated.

Collapse
 
scalfs profile image
Vitor Escalfoni

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.

Collapse
 
dimaportenko profile image
Dima Portenko

Welcome!

Collapse
 
wmonecke profile image
Walter Monecke

I dont know what I am doing wrong but this is not working for me.

Collapse
 
dimaportenko profile image
Dima Portenko

if you'll share your code on github or expo snack, I can take a look.