npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

reels-kit

v0.1.3

Published

Instagram Reels-style vertical video feed for React Native: snap paging, only nearby players mounted, drag-to-seek bar and tap gestures. Built on FlashList.

Readme

React Native Reels Kit

A highly customizable React Native toolkit for building Instagram Reels-style vertical video feeds, with snap paging, tap and scrub gestures and pluggable video adapters, built on FlashList.

npm downloads bundlephobia React Native TypeScript license CI GitHub stars

You get the hard parts: snap paging, only a few players mounted at a time, playback that pauses when it should, tap and scrub gestures, and retry on failed loads. The layout around the video is yours to build.

  • Headless building blocks, no fixed layout. Compose the cell from small components (action buttons, avatar, follow button, caption, scrub bar, overlays) and style them freely.
  • Bring your own video engine. The core imports no video library. A react-native-video adapter ships as reels-kit/react-native-video; you can write your own adapter for anything else.
  • Bounded memory. Only the active reel and its neighbours (mediaRenderRadius) mount a native player.
  • No bundled icons or assets. Every icon is a ReactNode you pass in.

Demo

| Paging | Tap to mute | Double tap to like | | :---: | :---: | :---: | | Paging | Tap to mute | Double tap to like |

| Scrub bar | Follow and caption | | :---: | :---: | | Scrub bar | Follow and caption |

Status

Initial release (0.1.0). The feed, gestures and components are ready to use on iOS and Android. As a 0.x version, the API may still change in minor releases.

Tested on the iOS simulator and on an Android 16 emulator, each in an empty React Native 0.85 app: paging and snapping, video playback through the react-native-video adapter, single tap, double tap, scrubbing and seeking, the mute indicator and the headless components. On Android, the load-error state with retry and loading more through onEndReached were checked as well. useCursorPagination and useFeedTargetItem are covered by unit tests.

Not tested yet: physical devices, right-to-left layouts, pull to refresh, onItemImpression and the imperative ref.

Requirements

  • React Native >=0.82, React >=19, New Architecture only
  • @shopify/flash-list >=2
  • react-native-reanimated >=4 and react-native-worklets
  • react-native-gesture-handler >=3

Reanimated has to match your React Native version. For example, Reanimated 4.7 requires React Native 0.86+, while 4.5.3 supports 0.83 to 0.86. If npm install reports a peer conflict, pin an older Reanimated that supports your React Native.

Tested with React Native 0.85.0, FlashList 2.3.2, Reanimated 4.5.3, worklets 0.11.4, Gesture Handler 3.2.1 and react-native-video 6.19.2.

Installation

# npm
npm install reels-kit @shopify/flash-list react-native-reanimated react-native-worklets react-native-gesture-handler

# yarn
yarn add reels-kit @shopify/flash-list react-native-reanimated react-native-worklets react-native-gesture-handler

# pnpm
pnpm add reels-kit @shopify/flash-list react-native-reanimated react-native-worklets react-native-gesture-handler

# bun
bun add reels-kit @shopify/flash-list react-native-reanimated react-native-worklets react-native-gesture-handler

If you use the bundled react-native-video adapter, install it yourself. reels-kit never installs a video engine for you:

# npm
npm install react-native-video

# yarn
yarn add react-native-video

# pnpm
pnpm add react-native-video

# bun
bun add react-native-video

Then:

  1. Add the worklets plugin to babel.config.js (it must be listed last):

    module.exports = {
      presets: ['module:@react-native/babel-preset'],
      plugins: ['react-native-worklets/plugin'],
    };
  2. Wrap your app in GestureHandlerRootView (see the example). ScrubBar and the gesture hooks need it.

  3. Install the pods: cd ios && pod install. On React Native 0.85, keep the project path free of spaces or pod install fails.

Quick start

import { useCallback, useRef, useState } from 'react';
import { StyleSheet, View } from 'react-native';
import {
  GestureDetector,
  GestureHandlerRootView,
} from 'react-native-gesture-handler';
import { useRecyclingState } from '@shopify/flash-list';
import {
  FeedActionButton,
  FeedCaption,
  FeedVideoSlot,
  MuteIndicatorOverlay,
  ReelsFeed,
  ScrubBar,
  useFeedTapGesture,
  usePlaybackGate,
} from 'reels-kit';
import type { ReelsFeedRenderItemInfo, ReelVideoHandle } from 'reels-kit';
import { RNVideoAdapter } from 'reels-kit/react-native-video';

interface Reel {
  id: string;
  uri: string;
  caption: string;
}

const REELS: Reel[] = [
  { id: '1', uri: 'https://example.com/a.mp4', caption: 'First reel' },
  { id: '2', uri: 'https://example.com/b.mp4', caption: 'Second reel' },
];

type CellProps = Omit<ReelsFeedRenderItemInfo<Reel>, 'index'> & {
  muted: boolean;
  onToggleMute: () => void;
};

function ReelCell({
  item,
  isActive,
  isFocused,
  isForeground,
  shouldRenderMedia,
  muted,
  onToggleMute,
}: CellProps) {
  // Pause unless this is the active reel, the screen is focused and the app
  // is in the foreground.
  const paused = usePlaybackGate({ isActive, isFocused, isForeground });
  const videoRef = useRef<ReelVideoHandle>(null);

  // Per-cell state that resets when FlashList recycles the cell for a new reel.
  const [time, setTime] = useRecyclingState(0, [item.id]);
  const [duration, setDuration] = useRecyclingState(0, [item.id]);
  const [flash, setFlash] = useRecyclingState(0, [item.id]);

  const gesture = useFeedTapGesture({
    onSingleTap: () => {
      onToggleMute();
      setFlash((n) => n + 1);
    },
  });

  return (
    <View style={styles.cell}>
      <GestureDetector gesture={gesture}>
        <View style={StyleSheet.absoluteFill}>
          {shouldRenderMedia ? (
            <FeedVideoSlot
              ref={videoRef}
              VideoComponent={RNVideoAdapter}
              sourceUri={item.uri}
              paused={paused}
              muted={muted}
              onLoad={(e) => setDuration(e.durationSec)}
              onProgress={(e) => setTime(e.currentTimeSec)}
            />
          ) : null}
        </View>
      </GestureDetector>

      <MuteIndicatorOverlay isMuted={muted} triggerKey={flash} />
      <FeedCaption text={item.caption} style={styles.caption} />
      <ScrubBar
        durationSec={duration}
        currentTimeSec={time}
        onSeek={(seconds) => videoRef.current?.seek(seconds)}
        style={styles.scrubBar}
      />
    </View>
  );
}

export default function App() {
  const [muted, setMuted] = useState(true);
  const toggleMute = useCallback(() => setMuted((m) => !m), []);

  const renderItem = useCallback(
    ({ item, isActive, ...rest }: ReelsFeedRenderItemInfo<Reel>) => (
      <ReelCell
        item={item}
        isActive={isActive}
        isFocused={rest.isFocused}
        isForeground={rest.isForeground}
        shouldRenderMedia={rest.shouldRenderMedia}
        muted={muted}
        onToggleMute={toggleMute}
      />
    ),
    [muted, toggleMute]
  );

  return (
    <GestureHandlerRootView style={styles.root}>
      <ReelsFeed
        data={REELS}
        keyExtractor={(reel) => reel.id}
        renderItem={renderItem}
        extraData={muted}
      />
    </GestureHandlerRootView>
  );
}

const styles = StyleSheet.create({
  root: { flex: 1, backgroundColor: '#000' },
  cell: { flex: 1, backgroundColor: '#000' },
  caption: { position: 'absolute', left: 12, right: 72, bottom: 64 },
  scrubBar: { position: 'absolute', left: 0, right: 0, bottom: 34 },
});

renderItem receives { item, index, isActive, shouldRenderMedia, isFocused, isForeground }. Mount the video only while shouldRenderMedia is true. If your cell depends on state outside data (like muted above), pass it as extraData so the list re-renders.

A fuller demo with likes, an avatar, a follow button and custom SVG icons is in docs/empty-app-quickstart.md.

API

<ReelsFeed>

| Prop | Default | Description | | --- | --- | --- | | data, keyExtractor, renderItem | required | Items, a stable key, and the cell renderer. | | itemHeight | measured | Page height. Defaults to the measured container height, not the window height. | | mediaRenderRadius | 1 | How many items around the active one get shouldRenderMedia. | | drawDistance | page height | FlashList draw distance. | | viewabilityThreshold | 80 | Percent visible for an item to count as active. | | isFocused | true | Pass your navigation focus state (for example useIsFocused()). | | isForeground | tracked | Override the built-in app foreground tracking. | | onActiveIndexChange(index, item) | | Fires when the active page changes. | | onItemImpression(index, item) | | Fires once per item the first time it becomes active. | | onEndReached, onEndReachedThreshold | 0.5 | Load more. | | refreshing, onRefresh | | Pull to refresh. | | ListEmptyComponent, ListFooterComponent | | Passed to FlashList. | | extraData, style | | |

The ref exposes scrollToIndex, scrollToId, scrollToTop, getActiveIndex and getListRef.

Building blocks

| Export | Purpose | | --- | --- | | FeedVideoSlot | Shows a thumbnail until the video is ready, remounts to retry a failed load, and shows a buffering slot. Takes your VideoComponent. | | FeedActionButton | An icon with an optional count. icon is any ReactNode; countStyle styles the count. | | FeedAvatar | Circle avatar with a letter fallback. Styles: style, imageStyle, fallbackStyle, fallbackTextStyle. | | FeedFollowButton | Follow / Following pill. Styles: style, textStyle, followingStyle, followingTextStyle. | | FeedCaption | Caption that truncates with a "more" link. | | ScrubBar | Progress bar you can drag. Animates smoothly between progress updates (smoothingMs). | | MuteIndicatorOverlay | Centered icon on a 60% black circle that flashes on each tap. | | DoubleTapLikeOverlay | Heart burst for double tap. |

Hooks

| Hook | Purpose | | --- | --- | | usePlaybackGate({ isActive, isFocused, isForeground }) | Returns paused. | | useFeedTapGesture({ onSingleTap, onDoubleTap, enableDoubleTap }) | Single tap with optional double tap, for GestureDetector. | | useScrubGesture(...) | The gesture behind ScrubBar. | | useAppForegroundState() | Whether the app is in the foreground. | | useCursorPagination, useFeedTargetItem | Pagination and "open this item first" helpers. |

Customizing icons, avatar and follow button: docs/customization.md.

Video adapters

An adapter is a component with this shape, so any engine can be plugged in:

import type { ReelVideoComponent } from 'reels-kit';
// props: sourceUri, paused, muted, repeat, resizeMode, onLoad, onProgress,
//        onReadyForDisplay, onBuffer, onEnd, onError, audio, style
// ref:   { seek(seconds) }

RNVideoAdapter (from reels-kit/react-native-video) implements it on top of react-native-video.

Contributing

reels-kit is open for contributors. Bug reports, fixes, docs and new features are all welcome, whatever the size.

Places where help is most useful right now:

  • Testing on Android and on physical devices, and reporting what breaks
  • An expo-video adapter for Expo apps (only the react-native-video adapter exists today)
  • Right-to-left layouts, pull to refresh and the retry flow
  • Tests for useCursorPagination, useFeedTargetItem and the gesture hooks

Open an issue to discuss a change, or send a pull request.

License

Created and owned by arbab-io. Licensed under the Apache License, Version 2.0.

If you fork or redistribute this project, the license requires you to keep the NOTICE attribution and the copyright notices, and to state any changes you made.