Collapsible Card with React Native Reanimated

Dima Portenko - Jul 10 '23 - - Dev Community

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

. . . . . . . . . . . . . . .