You're building a vertical feed — TikTok-style swipe, full-screen slides, thousands of items. You reach for a carousel library and hit the wall: it renders all slides to the DOM, chokes on touch gestures, and bundles half the internet.
ReelKit renders 3 DOM nodes at any time — previous, current, next — whether you have 4 slides or 40,000. Zero dependencies in core. Touch-first with momentum and snap. ~3.7 kB gzipped.
Try it live on StackBlitz — no setup needed.
Quick start
npm install @reelkit/react
import { useState } from 'react';
import { Reel, ReelIndicator } from '@reelkit/react';
const slides = [
{ title: 'Discover', color: '#6366f1' },
{ title: 'Trending', color: '#8b5cf6' },
{ title: 'Following', color: '#ec4899' },
{ title: 'For You', color: '#14b8a6' },
];
export default function App() {
const [index, setIndex] = useState(0);
return (
<Reel
count={slides.length}
style={{ width: '100%', height: '100dvh' }}
direction="vertical"
enableWheel
useNavKeys
afterChange={setIndex}
itemBuilder={(i, _inRange, size) => (
<div
style={{
width: size[0],
height: size[1],
background: slides[i].color,
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
color: 'white',
fontSize: '2rem',
}}
>
{slides[i].title}
</div>
)}
>
<ReelIndicator count={slides.length} active={index} />
</Reel>
);
}
Swipe, keyboard arrows, mouse wheel — all work out of the box. The size prop is optional: omit it and ReelKit auto-measures via ResizeObserver.
Programmatic navigation
const apiRef = useRef<ReelApi>(null);
<Reel count={100} apiRef={apiRef} itemBuilder={(i) => <Slide index={i} />} />
<button onClick={() => apiRef.current?.prev()}>Prev</button>
<button onClick={() => apiRef.current?.next()}>Next</button>
<button onClick={() => apiRef.current?.goTo(50)}>Jump to 50</button>
Navigation methods return promises — await apiRef.current!.next() resolves when the animation completes, so you can chain transitions sequentially.
How it works
Virtualization
Only render what's visible. ReelKit computes a visible range from the current index:
Current index: 50, count: 10,000
Visible: [49, 50, 51] ← 3 DOM nodes, always
With loop mode, it wraps at boundaries:
Current index: 0, loop: true
Visible: [9999, 0, 1] ← seamless wrap
When you call goTo(5000) from index 0, it doesn't animate through 5,000 slides. It temporarily swaps the adjacent slide with the target, animates a single step, and resolves. One smooth transition — the virtualization handles the rest.
Signals, not React state
ReelKit implements its own reactive system:
- Signal — writable observable value
- ComputedSignal — lazy derived value (zero cost when unobserved)
- batch() — groups multiple updates into a single notification pass
The React <Reel> component subscribes to these signals in effects and uses flushSync() on each animation frame to apply transforms synchronously — bypassing React's default batching. Your React tree doesn't re-render during swipes — transforms update at 60fps without touching component state.
When an animation completes, the index and transform value update in a single batch() call — observers never see an intermediate state. Navigation methods (next(), goTo()) return promises that resolve on completion, so you can chain transitions or await before taking the next action.
Touch-first gestures
The gesture controller detects the dominant axis from the initial touch vector and locks to it. It tracks per-frame delta, cumulative distance, and velocity. A fast swipe (> 1400 px/s) or drag past the threshold triggers a slide change with snap-back animation.
Packages
| Package | What it does | Size (gzip) |
|---|---|---|
@reelkit/core |
Framework-agnostic engine | 3.7 kB |
@reelkit/react |
React components + hooks | 2.6 kB |
@reelkit/react-reel-player |
Full-screen video reel player | 3.8 kB |
@reelkit/react-lightbox |
Image & video gallery lightbox | 3.4 kB |
@reelkit/core is the engine — all slider logic, gesture detection, keyboard/wheel controllers, and the signal system. Zero dependencies. Framework-agnostic. Vue bindings are in progress.
Everything in core is factory functions, not classes — createSliderController, createGestureController, createKeyboardController, createWheelController. Plain closures, no this binding issues, better tree-shaking.
@reelkit/react bridges the core to React. The <Reel> component creates a SliderController once via useState initializer and never recreates it. <ReelIndicator> renders Instagram-style scrollable dot indicators.
Reel Player
A ready-made TikTok/Instagram Reels overlay:
import { ReelPlayerOverlay } from '@reelkit/react-reel-player';
import '@reelkit/react-reel-player/styles.css';
<ReelPlayerOverlay
isOpen={isOpen}
onClose={() => setIsOpen(false)}
content={items}
initialIndex={0}
/>
Videos autoplay when the slide becomes active, pause when swiped away. A shared video element is reused across slides for iOS sound continuity.
Lightbox
Full-screen image gallery with three transition modes (slide, fade, zoom-in), swipe-to-close, keyboard navigation, and fullscreen API:
import { LightboxOverlay } from '@reelkit/react-lightbox';
import '@reelkit/react-lightbox/styles.css';
<LightboxOverlay
isOpen={index !== null}
images={images}
initialIndex={index ?? 0}
onClose={() => setIndex(null)}
transition="fade"
/>
Video support is opt-in and tree-shakeable — image-only usage pays zero extra cost.
Both packages expose render props for controls, navigation, and slide content — replace anything you need.
Links
- Documentation & demos
- GitHub
- npm: @reelkit/core | @reelkit/react | @reelkit/react-reel-player | @reelkit/react-lightbox
- StackBlitz starter
If you're building a vertical feed, a reel player, or a gallery lightbox in React — give ReelKit a try. MIT licensed, open source.
Feedback, suggestions, and bug reports are welcome — open an issue or drop a comment below. And if ReelKit saved you some time, a GitHub star would mean a lot — it's a small thing, but it really helps the project get noticed.
Top comments (0)