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 
useCallbackanduseMemo - 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"androle="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-feedThen 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!