When I needed a smooth, high-performance vertical video feed in a React web project, I was surprised by how few complete solutions existed.

I found some great inspiration from this awesome React Native tutorial by @albertocabrerajr, which showed how to build a TikTok-style feed in React Native with Expo.

But for React DOM (the web) —

  • Most examples were for mobile native apps.
  • Many web versions lacked smooth auto-play, visibility handling, or performance optimizations.
  • Others were just CSS tricks without real video lifecycle control.

So, I decided to build something from scratch, tailored for the web:
react-vertical-feed — a clean, optimized, developer-friendly vertical video feed component for React web apps.

✨ Meet react-vertical-feed

react-vertical-feed is a lightweight React component that solves the key challenges of building a TikTok-style vertical video experience on the web.

It handles:

  • Smooth vertical scrolling
  • Automatic play and pause based on video visibility
  • Lazy loading and resource management
  • Cross-browser compatibility
  • Performance optimizations for buttery smooth scrolling

🔥 How It Works

1. Video Visibility with Intersection Observer

The component uses the Intersection Observer API to detect which video is visible, and automatically play or pause it.

useEffect(() => {
  const observer = new IntersectionObserver(entries => {
    entries.forEach(entry => {
      const index = parseInt(entry.target.getAttribute('data-index') || '0', 10);
      const item = items[index];

      const video = entry.target.querySelector('video') as HTMLVideoElement;
      if (entry.isIntersecting) {
        video?.play().catch(console.error);
        onItemVisible?.(item, index);
      } else {
        video?.pause();
        onItemHidden?.(item, index);
      }
    });
  }, { threshold });

  const mediaElements = containerRef.current?.querySelectorAll('[data-index]') || [];
  mediaElements.forEach(media => observer.observe(media));

  return () => observer.disconnect();
}, [items, onItemVisible, onItemHidden, threshold]);

2. Efficient State Management

Instead of heavy Redux or complex context, we use minimal local state:

const [loadingStates, setLoadingStates] = useState<Record<number, boolean>>({});
const [errorStates, setErrorStates] = useState<Record<number, boolean>>({});

3. Performance First

  • Memoization with useCallback and useMemo
  • Functional state updates to avoid stale closures
  • Clean observer setup and teardown
  • Lazy rendering: Only load what's necessary
const mediaElements = useMemo(
  () => items.map((item, index) => defaultRenderItem(item, index)),
  [items, defaultRenderItem]
);

♿️ Accessibility Built-In

The component includes essential accessibility features:

  • ARIA roles (role="feed" and role="region")
  • Keyboard navigation (arrow keys)
  • Screen reader support with proper labels
  • Focus management with tabIndex
<div
  role="feed"
  aria-label="Vertical video feed"
  tabIndex={0}
  onKeyDown={handleKeyDown}
  // ... other props
>

🧰 Development Setup

I kept the tooling modern and robust:

  • TypeScript for safer code
  • Rollup to bundle for both ESM and CommonJS
  • Jest + Testing Library for component testing
  • ESLint + Prettier for clean formatting
  • Husky for pre-commit hooks

Example Rollup config:

export default {
  input: 'src/index.ts',
  output: [
    { file: 'dist/index.js', format: 'cjs', sourcemap: true },
    { file: 'dist/index.esm.js', format: 'esm', sourcemap: true },
  ],
  // more config...
};

📚 What I Learned

  • Simplify early — Complex setups kill performance.
  • TypeScript is a must — Caught so many edge cases during development.
  • Visibility and resource management are critical for smooth feeds.
  • Test-driven development is key — especially when dealing with IntersectionObserver behaviors.

📦 How to Use react-vertical-feed

Installation:

npm install react-vertical-feed
# or
yarn add react-vertical-feed

Then use it like this:

import { VerticalFeed } from 'react-vertical-feed';

<VerticalFeed
  items={[
    { src: '/videos/video1.mp4', muted: true, controls: false },
    { src: '/videos/video2.mp4', muted: true, controls: false },
    // more items...
  ]}
/>

You can find the full source on GitHub and a demo here.
Contributions and feedback are welcome!