DEV Community

Cover image for Building a Reels UI in React Native 🎥 | Smooth Scrolling, Auto-Playing Videos & More!
Amit Kumar
Amit Kumar

Posted on

6 2 1 1 1

Building a Reels UI in React Native 🎥 | Smooth Scrolling, Auto-Playing Videos & More!

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!

Image description

🌟 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' },
});

Enter fullscreen mode Exit fullscreen mode

🎞 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;

Enter fullscreen mode Exit fullscreen mode

🎥 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,
  },
});

Enter fullscreen mode Exit fullscreen mode

🎭 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',
  },
});
Enter fullscreen mode Exit fullscreen mode

🎭 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,
  },
});
Enter fullscreen mode Exit fullscreen mode

❤️ 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,
  },
});

Enter fullscreen mode Exit fullscreen mode

🎬 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! 💬✨

Image of Datadog

Diagram Like A Pro

Bring your cloud architecture to life with expert tips from AWS and Datadog. In this ebook, AWS Solutions Architects Jason Mimick and James Wenzel reveal pro strategies for building clear, compelling diagrams that make an impact.

Get the Guide

Top comments (4)

Collapse
 
mitchell_vermaning_6de654 profile image
Mitchell

Great example, works well!

I was wondering what the purpose of visible and isNext is in FeedRow and VideoComponent. 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

Collapse
 
amitkumar13 profile image
Amit Kumar

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:

const VideoComponent = ({ data, isVisible, isNext }) => {
  if (!isVisible && !isNext) return null; // Only render if visible or next in line

  return (
    <Video
      source={{ uri: data.video }}
      autoPlay
      repeat
      resizeMode="cover"
      muted={!isVisible}
      playInBackground={false}
      paused={!isVisible}
      ignoreSilentSwitch="ignore"
      style={videoStyle}
    />
  );
};
Enter fullscreen mode Exit fullscreen mode
Collapse
 
mitchell_vermaning_6de654 profile image
Mitchell

Thanks so much for your helpful tips and clear explanation!

Based on your suggestion, I made a small tweak that worked really well for my use case. I used const { height } = useWindowDimensions(); together with the following check in my FeedRow:

const FeedRow = ({ data, index, isVisible, isNext }: IProps) => {
  ...
  const {height} = useWindowDimensions();

  if (!isVisible && !isNext) {
    return <View style={{ height }} />;
  }
  ...
}
Enter fullscreen mode Exit fullscreen mode

Using return null caused issues with scrolling, and some videos were still playing unexpectedly. It also caused other components like <FeedHeader />, <FeedSidebar />, and <FeedFooter /> to be rendered when they shouldn’t be. By returning a with the height, the layout remains intact, and now only the current and next videos are loaded and rendered properly.

Thanks again for the great article and your support!

Mitchell

Thread Thread
 
amitkumar13 profile image
Amit Kumar

Ahh, that's great! Thanks a lot for this, @mitchell_vermaning_6de654 — really appreciate it!

Image of Datadog

Keep your GPUs in check

This cheatsheet shows how to use Datadog’s NVIDIA DCGM and Triton integrations to track GPU health, resource usage, and model performance—helping you optimize AI workloads and avoid hardware bottlenecks.

Get the Cheatsheet

👋 Kindness is contagious

Engage with a wealth of insights in this thoughtful article, valued within the supportive DEV Community. Coders of every background are welcome to join in and add to our collective wisdom.

A sincere "thank you" often brightens someone’s day. Share your gratitude in the comments below!

On DEV, the act of sharing knowledge eases our journey and fortifies our community ties. Found value in this? A quick thank you to the author can make a significant impact.

Okay