Reels-style short videos have taken social media by storm! Want to build your own? In this guide, weβll create a seamless vertical video feed with auto-play, smooth scrolling, and interactive UI elementsβjust like Instagram Reels or TikTok!
π What We'll Build
A dynamic Reels UI with:
β
Auto-playing videos when visible
β
Smooth vertical scrolling with Animated.FlatList
β
Engaging UI elements (like, comment, share buttons)
β
Optimized playback performance
π Step-by-Step Implementation
π 1. ReelComponent: The Video Feed
Manages scrolling, video playback visibility, and rendering video rows dynamically.
import { Animated, StyleSheet, View, useWindowDimensions } from 'react-native';
import React, { useRef, useState, useCallback } from 'react';
import { VIDEO_DATA } from './data';
import FeedRow from './FeedRow';
const ReelComponent = () => {
const {height} = useWindowDimensions();
const scrollY = useRef(new Animated.Value(0)).current;
const [scrollInfo, setScrollInfo] = useState({isViewable: true, index: 0});
const refFlatList = useRef(null);
const viewabilityConfig = useRef({viewAreaCoveragePercentThreshold: 80});
const onViewableItemsChanged = useCallback(({changed}) => {
if (changed.length > 0) {
setScrollInfo({
isViewable: changed[0].isViewable,
index: changed[0].index,
});
}
}, []);
const getItemLayout = useCallback(
(_, index) => ({
length: height,
offset: height * index,
index,
}),
[height],
);
const keyExtractor = useCallback(item => `${item.id}`, []);
const onScroll = useCallback(
Animated.event([{nativeEvent: {contentOffset: {y: scrollY}}}], {
useNativeDriver: true,
}),
[],
);
const renderItem = useCallback(
({item, index}) => {
const {index: scrollIndex} = scrollInfo;
const isNext = Math.abs(index - scrollIndex) <= 1;
return (
<FeedRow
data={item}
index={index}
isNext={isNext}
visible={scrollInfo}
isVisible={scrollIndex === index}
/>
);
},
[scrollInfo],
);
return (
<View style={styles.flexContainer}>
<StatusBar barStyle={'light-content'} backgroundColor={'black'} />
<Animated.FlatList
pagingEnabled
showsVerticalScrollIndicator={false}
ref={refFlatList}
automaticallyAdjustContentInsets
onViewableItemsChanged={onViewableItemsChanged}
viewabilityConfig={viewabilityConfig.current}
onScroll={onScroll}
data={VIDEO_DATA}
renderItem={renderItem}
getItemLayout={getItemLayout}
decelerationRate="fast"
keyExtractor={keyExtractor}
onEndReachedThreshold={0.2}
removeClippedSubviews
bounces={false}
/>
</View>
);
};
export default ReelComponent;
const styles = StyleSheet.create({
flexContainer: { flex: 1, backgroundColor: 'black' },
});
π 2. FeedRow: Handling Individual Videos
Each video component, along with sidebars and footers, is wrapped inside FeedRow.
import { StyleSheet, View } from 'react-native';
import React from 'react';
import VideoComponent from './VideoComponent';
import FeedFooter from './FeedFooter';
import FeedSideBar from './FeedSideBar';
import FeedHeader from './FeedHeader';
const FeedRow = ({data, index, visible, isVisible, isNext}) => {
return (
<View>
<VideoComponent data={data} isNext={isNext} isVisible {isVisible} />
<FeedHeader index={index} />
<FeedSideBar data={data} />
<FeedFooter data={data} />
</View>
);
};
export default FeedRow;
π₯ 3. VideoComponent: Auto-Playing Video Logic
Ensures smooth playback by muting & pausing videos when out of view.
import { StyleSheet, useWindowDimensions } from 'react-native';
import React, { useMemo } from 'react';
import Video from 'react-native-video';
const VideoComponent = ({data, isVisible}) => {
const {height} = useWindowDimensions();
const videoStyle = useMemo(() => styles.video(height), [height]);
return (
<>
<Video
source={{uri: data.video}}
autoPlay
repeat
resizeMode="cover"
muted={!isVisible}
playInBackground={false}
paused={!isVisible}
ignoreSilentSwitch="ignore"
style={videoStyle}
/>
<LinearGradient
colors={[
'#000000F0',
'#000000D0',
'#000000A0',
'#00000070',
'#00000040',
]}
start={{x: 0, y: 0}}
end={{x: 0, y: 0.5}}
style={styles.controlsContainer}
/>
</>
);
};
export default VideoComponent;
const styles = StyleSheet.create({
video: height => ({
backgroundColor: 'black',
width: '100%',
height: Platform.OS === 'ios' ? height : height - 50,
}),
controlsContainer: {
...StyleSheet.absoluteFillObject,
},
});
π 4. UI Components: Making It Engaging
π· FeedHeader: The Title & Camera Icon
import { SafeAreaView, StyleSheet, Text } from 'react-native';
import React from 'react';
import { CameraIcon } from '../../assets';
const FeedHeader = ({ index }) => {
return (
<SafeAreaView style={styles.container}>
{index === 0 && <Text style={styles.title}>Reels</Text>}
<CameraIcon />
</SafeAreaView>
);
};
export default FeedHeader;
const styles = StyleSheet.create({
container: {
flexDirection: 'row',
alignItems: 'center',
position: 'absolute',
top: Platform.OS === 'ios' ? 65 : 10,
marginHorizontal: 20,
},
alignRight: {
alignSelf: 'flex-end',
right: 5,
},
title: {
color: '#fff',
flex: 1,
fontSize: 24,
fontWeight: '700',
},
});
π 5. UI Components: Making It Engaging
π· FeedFooter
const FeedFooter = ({ data }) => {
const { thumbnailUrl, title, description, isLive, friends } = data;
const followerCount = Math.floor(Math.random() * 20) + 1;
return (
<View style={styles.container}>
<View style={styles.profileContainer}>
<Image source={{ uri: thumbnailUrl }} style={styles.thumbnail} resizeMode="cover" />
<View style={styles.userInfo}>
<View style={styles.userNameContainer}>
<Text style={styles.nameStyle}>{title}</Text>
{isLive && <TickIcon />}
</View>
<View style={styles.audioContainer}>
<MusicIcon width={10} height={10} />
<Text style={styles.audioText}>Original audio</Text>
</View>
</View>
<View style={styles.followButton}>
<Text style={styles.followText}>Follow</Text>
</View>
</View>
<Text numberOfLines={2} style={styles.desc}>
{description}
</Text>
<View style={styles.friendsContainer}>
{friends.map((item, index) => (
<Image key={index} source={{ uri: item.imageUrl }} style={styles.friendImage} />
))}
<Text style={styles.followInfo}>{`Followed by Akash and ${followerCount} others`}</Text>
</View>
</View>
);
};
export default FeedFooter;
const styles = StyleSheet.create({
container: {
position: 'absolute',
bottom: Platform.OS === 'ios' ? 120 : 90,
marginLeft: 20,
},
profileContainer: {
flexDirection: 'row',
alignItems: 'center',
marginBottom: 12,
},
thumbnail: {
width: 30,
height: 30,
borderRadius: 20,
overflow: 'hidden',
},
userInfo: {
marginLeft: 10,
},
userNameContainer: {
flexDirection: 'row',
alignItems: 'center',
},
nameStyle: {
color: '#fff',
fontSize: 12,
fontWeight: '700',
marginRight: 4,
},
audioContainer: {
flexDirection: 'row',
alignItems: 'center',
marginTop: 2,
},
audioText: {
color: '#fff',
marginLeft: 6,
},
followButton: {
marginLeft: 24,
borderWidth: 1,
borderColor: '#fff',
borderRadius: 8,
paddingHorizontal: 8,
paddingVertical: 2,
},
followText: {
color: '#fff',
},
desc: {
color: '#fff',
width: 300,
},
friendsContainer: {
flexDirection: 'row',
alignItems: 'center',
marginTop: 10,
},
friendImage: {
width: 15,
height: 15,
borderRadius: 150,
marginRight: -5,
},
followInfo: {
color: '#fff',
marginLeft: 13,
fontSize: 12,
},
});
β€οΈ FeedSideBar: Like, Comment, and Share Buttons
const IconWithText = ({IconComponent, count}) => (
<View style={styles.iconContainer}>
<IconComponent />
<Text style={styles.countText}>{count}</Text>
</View>
);
const FeedSideBar = ({data}) => {
const {likes, comments, shares, thumbnailUrl} = data;
return (
<View style={styles.container}>
<IconWithText IconComponent={HeartIcon} count={likes} />
<IconWithText IconComponent={CommentIcon} count={comments} />
<IconWithText IconComponent={ShareIcon} count={shares} />
<MenuIcon />
<View style={styles.thumbnailContainer}>
<Image
source={{uri: thumbnailUrl}}
style={styles.thumbnail}
resizeMode="cover"
/>
</View>
</View>
);
};
export default FeedSideBar;
const styles = StyleSheet.create({
container: {
position: 'absolute',
bottom: Platform.OS === 'ios' ? 120 : 90,
alignSelf: 'flex-end',
alignItems: 'center',
gap: 20,
right: 20,
},
iconContainer: {
alignItems: 'center',
},
countText: {
color: '#fff',
marginTop: 10,
},
thumbnailContainer: {
borderWidth: 3,
borderColor: '#fff',
borderRadius: 8,
overflow: 'hidden',
},
thumbnail: {
width: 24,
height: 24,
borderRadius: 8,
},
});
π¬ Final Thoughts
Congratulations! π Youβve built a sleek, high-performance Reels UI in React Native. With auto-playing videos, smooth scrolling, and interactive UI elements, this implementation is perfect for any social media or short-video app.
π Ready to take it further? Try adding:
π₯ Swipe gestures for seamless navigation
π Caching videos for smoother playback
πΆ Background music support
Now go build your own viral Reels app! π
π‘ Got questions or improvements? Drop a comment below! π¬β¨
Top comments (2)
Great example, works well!
I was wondering what the purpose of
visible
andisNext
is inFeedRow
andVideoComponent
. They donβt seem to be used at the moment. Can I use them to prevent all videos from my data array from loading at once? Right now, 10 videos load simultaneously, even though the user only sees the first one.Thanks in advance!
Mitchell
You're absolutely right to notice that visible and isNext aren't being actively used in the provided code. Their intended purpose is likely to optimize video rendering by controlling which videos should be actively playing or preloaded.
If you're experiencing all 10 videos loading at once, you can leverage these props to limit loading to only the currently visible video and the next one for a smoother experience. Try modifying VideoComponent like this: