Instagram / TikTok-style vertical swipe pager for React. A smooth, virtualized, SSR-safe vertical feed — render any view per slot.
- Gesture-driven — configurable swipe threshold, velocity flick, edge resistance, and per-input controls. Touch, mouse, wheel, and keyboard all drive one engine.
- Virtualized — only the active item ± overscan are mounted.
- Data-change safe — appending never reflows existing items; opt in to key-based active item preservation for prepends/reorders.
- Zero runtime dependency — no animation library, no CSS import.
- SSR-safe — works in Next.js (App Router and Pages).
- Rich callbacks — swipe, item enter/leave lifecycle, prefetch hints, and left/center/right press-hold + tap zones.
- No re-render on drag — the track transform is written imperatively via
requestAnimationFrame.
pnpm add short-form-view # react >= 18 is a peer dependency'use client'
import { useCallback, useState } from 'react'
import { ShortFormView } from 'short-form-view'
type Clip = { id: string; src: string }
export default function Feed() {
const [clips, setClips] = useState<Clip[]>(initialClips)
const loadMore = useCallback(() => {
fetchMore().then((more) => setClips((prev) => [...prev, ...more]))
}, [])
return (
<ShortFormView
data={clips}
keyExtractor={(c) => c.id}
threshold={0.2} // commit a swipe at 20% of the viewport
onSwiped={({ from, to, direction }) => track(from, to, direction)}
onIndexChange={(i, { reason }) => console.log('now at', i, 'via', reason)}
onEndReached={loadMore} // append more, no reflow
renderItem={(clip, state) => (
<video
src={clip.src}
muted
loop
autoPlay={state.isActive} // play only while focused
style={{ width: '100%', height: '100%', objectFit: 'cover' }}
/>
)}
/>
)
}renderItem returns any React node, so a slot can be a video, an image, or any custom component — the library just connects them into the swipe feed.
| Navigation | drag · wheel/trackpad · keyboard (arrows, page, home/end) · controlled & uncontrolled index · imperative ref (scrollToIndex / next / prev) · optional key-preserved data changes |
| Tuning | threshold (fraction or px) · velocityThreshold · resistance · loop · transitionDuration · easing · overscan · swipeEnabled / wheelEnabled / keyboardEnabled |
| Callbacks | onSwiped · onIndexChange · onItemEnter / onItemLeave · onPrefetch · onEndReached · onHoldStart / onHoldEnd / onTapZone |
| Per-item state | isActive · isVisible · isSnapping · distance · index / activeIndex |
Full props, types, and the imperative handle: see the package README.
The active slot is overlaid with left / center / right zones. A stationary press past holdDelay fires onHoldStart (pause the video, dim the UI); a quick press-release fires onTapZone. Any vertical movement past a small slop becomes a swipe instead — so holding and swiping never fight.
Interactive descendants are protected by default: buttons, links, form controls, editable content, and elements marked with data-sfv-ignore-gesture do not start feed gestures.
| Path | What |
|---|---|
packages/short-form-view |
the library — gesture engine, virtualization, item lifecycle, SSR utils |
examples/next-demo |
Next.js App Router demo with live controls and infinite append |
docs/superpowers |
design spec + implementation plan |
pnpm install
pnpm -C examples/next-demo dev # http://localhost:3100
pnpm test # unit (vitest)
pnpm -C packages/short-form-view test:cov
pnpm -C examples/next-demo test:e2e # Playwright E2E + visual