Animating the Accordion in React Native

Hemanta Sapkota
ReactNativePro
Published in
4 min readJun 19, 2024

Creating smooth, interactive user interfaces is a key part of mobile app development. One common UI pattern is the accordion, which allows users to expand and collapse sections of content. In this post, we’ll walk through how to animate an accordion in React Native, ensuring it provides a visually appealing experience. We’ll use the code snippet provided to guide our explanation.

Animation Preview

Setting Up

First, let’s import the necessary modules and initialize our environment. We’ll need React, state and effect hooks, and several components from React Native. For animations, we’ll use the Animated API and LayoutAnimation.

import {LucideChevronDown, LucideChevronUp} from 'lucide-react-native';
import React, {useCallback, useImperativeHandle, useState, useRef, useEffect} from 'react';
import {
StyleSheet,
Text,
View,
Animated,
LayoutAnimation,
Platform,
UIManager,
TouchableWithoutFeedback,
} from 'react-native';

The conditional statement ensures that layout animations are enabled on Android devices.

if (Platform.OS === 'android' && UIManager.setLayoutAnimationEnabledExperimental) {
UIManager.setLayoutAnimationEnabledExperimental(true);
}

Defining the Accordion Component

We define our Accordion component using React.forwardRef to allow parent components to control the accordion state if necessary.

type Props = {
title: string;
children: React.ReactNode;
};

export const Accordion = React.forwardRef((props: Props, ref) => {
const {title, children} = props;
const [isOpen, setIsOpen] = useState(false);
const heightAnim = useRef(new Animated.Value(0)).current;

const toggleOpen = useCallback(() => {
LayoutAnimation.configureNext(LayoutAnimation.Presets.easeInEaseOut);
setIsOpen(!isOpen);
}, [isOpen]);

useImperativeHandle(
ref,
() => ({
open: () => {
if (isOpen) {
return;
}
return toggleOpen();
},
}),
[toggleOpen, isOpen],
);

Here, we use useState to track whether the accordion is open, and useRef to hold an animated value for the height. The toggleOpen function toggles the isOpen state and configures a layout animation to smoothly animate the change.

We’ll discuss more about useImpreativeHandle in the coming section.

Rendering the Accordion

Next, we define the rendering logic, including the title, the toggle icon, and the content area.


return (
<View style={styles.accordionSection}>
<TouchableWithoutFeedback onPress={toggleOpen} accessibilityRole="button" accessibilityLabel="Toggle Accordion">
<View style={[styles.accordionTitle, {flexDirection: 'row', justifyContent: 'space-between'}]}>
<Text style={styles.accordionHeader}>{title}</Text>
{isOpen ? (
<LucideChevronUp color="black" />
) : (
<LucideChevronDown color="black" />
)}
</View>
</TouchableWithoutFeedback>
<Animated.View style={[styles.accordionContent]}>
{isOpen && <View>{children}</View>}
</Animated.View>
</View>
);
});

The TouchableWithoutFeedback component wraps the title and icon, allowing the user to toggle the accordion by tapping anywhere in this area. The Animated.View component wraps the children, ensuring any changes to its height are animated.

Styling the Accordion

Lastly, we define the styles for the accordion.


const styles = StyleSheet.create({
accordionSection: {
marginBottom: 16,
},
accordionTitle: {
padding: 10,
backgroundColor: '#f0f0f0',
},
accordionContent: {
overflow: 'hidden',
backgroundColor: '#E0E0E0',
},
accordionHeader: {
fontSize: 20,
fontWeight: 'bold',
},
});

Why Use useImperativeHandle?

In the provided React Native accordion component, the useImperativeHandle hook is employed to expose imperative methods to parent components. This allows the parent components to control the internal state of the AccordionSection, such as opening or closing it programmatically.

Purpose of useImperativeHandle

  1. Controlled Component: It enables the creation of a controlled component by exposing methods that can alter its internal state from the outside.
  2. Encapsulation: It provides a clean and encapsulated way to interact with the component, ensuring that the internal implementation details are hidden while exposing specific functionalities.
  3. Enhanced Interactivity: Allows for more complex interactions and behaviors by letting parent components directly influence the child component’s state.

How useImperativeHandle Works

useImperativeHandle accepts two arguments:

  • ref: The ref that is passed from the parent component.
  • createHandle: A function that returns an object with the methods to be exposed.

In our Accordion component:

useImperativeHandle(
ref,
() => ({
open: () => {
if (isOpen) {
return;
}
return toggleOpen();
},
}),
[toggleOpen, isOpen],
);
  • Ref Argument: ref is forwarded from the parent component, allowing it to attach to the accordion component.
  • Create Handle Function: The function returns an object containing the open method. This method toggles the accordion open state.

Example Scenario

Imagine a scenario where you have multiple accordions and a “Expand All” button in the parent component. Using useImperativeHandle, you can programmatically open all accordions from the parent:

const ParentComponent = () => {
const accordionRef = useRef(null);

const handleExpandAll = () => {
accordionRef.current.open();
};

return (
<View>
<Button title="Expand All" onPress={handleExpandAll} />
<AccordionSection ref={accordionRef} title="Accordion 1">
<Text>Content 1</Text>
</AccordionSection>
{/* Additional AccordionSections */}
</View>
);
};

In this setup, useImperativeHandle empowers the parent component with the ability to control the accordion’s state, enhancing flexibility and interactivity.

Full source code

import {LucideChevronDown, LucideChevronUp} from 'lucide-react-native';
import React, {useCallback, useImperativeHandle, useState } from 'react';
import {
StyleSheet,
Text,
View,
Animated,
LayoutAnimation,
Platform,
UIManager,
TouchableWithoutFeedback,
} from 'react-native';

if (
Platform.OS === 'android' &&
UIManager.setLayoutAnimationEnabledExperimental
) {
UIManager.setLayoutAnimationEnabledExperimental(true);
}

type Props = {
title: string;
children: React.ReactNode;
};

export const Accordion = React.forwardRef((props: Props, ref) => {
const {title, children} = props;
const [isOpen, setIsOpen] = useState(false);

const toggleOpen = useCallback(() => {
LayoutAnimation.configureNext(LayoutAnimation.Presets.easeInEaseOut);
setIsOpen(!isOpen);
}, [isOpen]);

useImperativeHandle(
ref,
() => ({
open: () => {
if (isOpen) {
return;
}
return toggleOpen();
},
}),
[toggleOpen, isOpen],
);

return (
<View style={styles.accordionSection}>
<TouchableWithoutFeedback
onPress={toggleOpen}
accessibilityRole="button"
accessibilityLabel="Toggle Accordion">
<View
style={[
styles.accordionTitle,
{flexDirection: 'row', justifyContent: 'space-between'},
]}>
<Text style={styles.accordionHeader}>{title}</Text>
{isOpen ? (
<LucideChevronUp color="black" />
) : (
<LucideChevronDown color="black" />
)}
</View>
</TouchableWithoutFeedback>
<Animated.View style={[styles.accordionContent]}>
{isOpen && <View>{children}</View>}
</Animated.View>
</View>
);
});

const styles = StyleSheet.create({
accordionSection: {
marginBottom: 16,
},
accordionTitle: {
padding: 10,
backgroundColor: '#f0f0f0',
},
accordionContent: {
overflow: 'hidden',
backgroundColor: '#E0E0E0',
},
accordionHeader: {
fontSize: 20,
fontWeight: 'bold',
},
});

Conclusion

The accordion component is particularly important in React Native for displaying a large amount of information in a compact form. It allows developers to present extensive content in a well-organized manner, reducing clutter and improving readability. By expanding and collapsing sections, users can quickly access the information they need without being overwhelmed. This approach not only enhances the user experience but also helps in maintaining a clean and efficient design, crucial for mobile applications where screen space is limited.

Stay tuned for more React Native tips and tutorials!

--

--