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.
30 May 2025
5 min read
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.
Why Dynamic Height?
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:
- Give the FlatList a fixed height (🫤 UX).
- Measure each item’s height and animate the list container height to match it (👌 yes, please!).
Tools of the Trade
- FlatList for horizontal pagination
- onLayout to measure item height
- react-native-reanimated for smooth animations
- Animated.FlatList and withTiming for animated transitions
Key Concepts and Logic
Measure Item Height Dynamically
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).
Store Heights Indexed by Item
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.
Track Scroll Position Using Reanimated (Optional)
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.
Calculate and Animate the Height
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:
- We compute the current index by dividing scroll offset by screen width. (Optional, use your preferred approach here)
- We fetch the pre-measured height for that index.
- If the height exists, we animate listHeight to that height using withTiming.
- The return value is applied as the FlatList’s style: { height: listHeight.value }
Bonus: withTiming makes the transition smooth. No jank!
Putting It All Together
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> ); });
Final Result
What you get is a FlatList that:
- Scrolls horizontally with pagingEnabled
- Measures and stores each item’s height
- Animates the outer container height smoothly between screens
This is great for quizzes, walkthroughs, onboarding screens, and any horizontally scrollable pages with content that varies in size.
Gotchas and Tips
- Make sure every item actually renders before it’s measured.
- Avoid updating state on every scroll — only update height when it’s different.
- Use useMemo or useCallback to prevent unnecessary renders.
- You can cache height measurements for performance (if items don’t change
Wrapping Up
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.
Want to see a demo?
Check out this snack
Happy coding! 🚀