Native
Animating FlatList Height Dynamically in React Native (In-Depth Guide)
Learn how to build a horizontal FlatList in React Native that smoothly adjusts its height based on each item’s content using onLayout and Reanimated animations.
Native
Learn how to build a horizontal FlatList in React Native that smoothly adjusts its height based on each item’s content using onLayout and Reanimated animations.
Most of us are familiar with FlatList, the backbone of performant scrollable lists in React Native. But when you start customizing things—like making the height of each page dynamic based on its content in a horizontally paged FlatList—it can get tricky.
In this post, I’ll walk you through how to dynamically animate the height of a horizontal FlatList based on each item’s content height. This makes your UI not just look polished but feel buttery smooth.
By default, FlatList uses the same height for all its items (Height of the largest element). If your list is paginated horizontally (pagingEnabled), and each “page” (item) has a different height, you’ll run into two options:
Each item in the FlatList has different height content. To adjust the container’s height, we must first know each item’s height.
We use React Native’s onLayout callback to measure this:
tsxconst handleLayout = useCallback((e: LayoutChangeEvent) => { if (hasMeasured.current) return; const height = e.nativeEvent.layout.height; onHeightMeasured(index, height); hasMeasured.current = true; }, [index, onHeightMeasured]);
This function is triggered once per item, and reports the height back to the parent FlatList via a prop function onHeightMeasured.
hasMeasured ensures we don’t keep updating the height over and over (which would cause performance issues).
In the parent, we store item heights like this:
tsxconst [itemHeight, setItemHeight] = useState<{ [key: number]: number }>({});
Then, when a child item reports its height:
tsxconst handleOnHeightMeasured = useCallback((index: number, height: number) => { setItemHeight((prev) => ({ ...prev, [index]: height })); if (index === 0) { listHeight.value = height; // Set initial height } }, []);
This gives us a mapping from FlatList index → measured height.
We set the height of the first element instantly without any transition effects to guarantee that the initial item displays correctly.
We want the FlatList’s height to change as you swipe between pages. To do that, we need to know which page is visible.
We use useAnimatedScrollHandler to track scroll position:
tsxconst scrollOffset = useSharedValue(0); const handleScroll = useAnimatedScrollHandler((e) => { scrollOffset.value = e.contentOffset.x; });
This gives us live access to the scroll position, which we can use to calculate the current index.
Optional: You can employ alternative methods to determine the current index.
We use useAnimatedStyle to dynamically compute and animate the FlatList’s container height:
tsxconst rFlatlistStyle = useAnimatedStyle(() => { const index = Math.round(scrollOffset.value / width); // or use the current index from your preferred approach const height = itemHeight[index]; if (height) { listHeight.value = withTiming(height, { duration: 300 }); } return { height: listHeight.value }; });
Let’s break this down:
Bonus: withTiming makes the transition smooth. No jank!
tsxexport default function App() { const { width } = useWindowDimensions(); const [itemHeight, setItemHeight] = useState<{ [key: number]: number }>({}); const listHeight = useSharedValue(0); const scrollOffset = useSharedValue(0); // Scroll handler to update the scrollOffset value const handleScroll = useAnimatedScrollHandler((e) => { scrollOffset.value = e.contentOffset.x; // Update the session store scroll offset }); // Handle height measurement of item to adjust the list height dynamically const handleOnHeightMeasured = useCallback( (index: number, height: number) => { setItemHeight((prev) => ({ ...prev, [index]: height, })); if (index === 0) { listHeight.value = height; } }, [listHeight] ); // Animated style for the FlatList to adjust its height based on the current question const rFlatlistStyle = useAnimatedStyle(() => { const index = Math.round(scrollOffset.value / width); const height = itemHeight[index]; if (height) { listHeight.value = withTiming(height, { duration: 300, //duration of animation }); } return { height: listHeight.value }; }); return ( <Animated.FlatList pagingEnabled horizontal scrollEventThrottle={16} showsHorizontalScrollIndicator={false} onScroll={handleScroll} data={mockData} style={ rFlatlistStyle} renderItem={(props) => ( <RenderItem {...props} onHeightMeasured={handleOnHeightMeasured} /> )} /> ); } interface Props extends ListRenderItemInfo<typeof mockData> { onHeightMeasured: (index: number, height: number) => void; } export const RenderItem = memo(({ item, index, onHeightMeasured }: Props) => { const { width } = useWindowDimensions(); const hasMeasured = useRef(false); const handleLayout = useCallback( (e: LayoutChangeEvent) => { if (hasMeasured.current) return; // Prevent multiple measurements const height = e.nativeEvent.layout.height; onHeightMeasured(index, height); }, [index, onHeightMeasured] ); return ( <View onLayout={handleLayout} style={{ width }}> {content} // your content here with the dynamic height </View> ); });
What you get is a FlatList that:
This is great for quizzes, walkthroughs, onboarding screens, and any horizontally scrollable pages with content that varies in size.
Animating FlatList height dynamically is a clean and elegant UX trick that separates polished apps from average ones. With just onLayout, a bit of useSharedValue, and a smooth withTiming, you can build scrollable lists that feel alive and adaptive.
Check out this snack
Happy coding! 🚀