Skip to content
briansunterPublic

About

React Tiktok Component

Resources

Contributing

Stars

2 stars

Watchers

0 watching

Forks

Latest commit

 

History

25 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

swipefeed: headless React primitives for swipe feeds

swipefeed

CI Status Coverage

Documentation | Live Example

Headless React primitives for building TikTok/Reels-style swipe feeds. Ships a render-prop component (SwipeDeck) and a hook (useSwipeDeck) that wire up native scroll-snap, TanStack virtualization, gesture/wheel/keyboard inputs, accessibility, and programmatic controls.

How swipefeed works: pointer / wheel / keyboard / programmatic inputs flow into a core engine that normalizes intent, resolves target index, manages animation, and updates state, then layout & virtualization (TanStack Virtual) emit native scroll-snap output with accessibility roles and end-reached callbacks

Highlights

  • Headless: bring your own markup/styles; spread provided props for behavior.
  • Works vertically or horizontally with LTR/RTL awareness.
  • Virtualization via @tanstack/react-virtual with auto measurement and overscan tuning.
  • Input support: pointer drag/flick, discrete wheel paging with heavy dampening, keyboard (global or scoped).
  • Controlled or uncontrolled index, loopable navigation, programmatic prev/next/scrollTo.
  • Animation-frame-throttled motion snapshots for synchronized media and visual effects.
  • Position- or intersection-based activation with exact active/inactive lifecycle callbacks.
  • Accessibility-first: focusable role="feed" viewport, per-item role="article", aria-label, aria-busy, snap alignment.
  • Reduced-motion aware (prefers-reduced-motion uses instant scrolling).
  • End-reached callbacks for infinite loading triggers.

Installation

Using your preferred package manager:

bun add swipefeed
# or
npm install swipefeed

Peer dependencies: react@^19, react-dom@^19.

Quick start (component)

import { SwipeDeck } from "swipefeed";

function Feed({ items }) {
  return (
    <SwipeDeck items={items} className="w-full h-screen overflow-hidden">
      {({ item, isActive, props }) => (
        <section
          {...props}
          style={{ height: "100vh" }} // make each slide fill the viewport
        >
          <VideoPlayer video={item} playing={isActive} />
        </section>
      )}
    </SwipeDeck>
  );
}

Quick start (hook)

import { useSwipeDeck } from "swipefeed";

function CustomLayout({ items }) {
  const deck = useSwipeDeck({ items, orientation: "horizontal", direction: "rtl" });
  const viewportProps = deck.getViewportProps();

  return (
    <div {...viewportProps} style={{ ...viewportProps.style, height: "100vh" }}>
      <div style={{ width: deck.totalSize, position: "relative" }}>
        {deck.virtualItems.map((virtual) => {
          const props = deck.getItemProps(virtual.index);
          return (
            <article key={virtual.key} {...props}>
              {items[virtual.index].title}
            </article>
          );
        })}
      </div>
    </div>
  );
}

Styling essentials

  • Give the viewport a fixed height/width (e.g., h-screen); items are absolutely positioned with transforms supplied by getItemProps.
  • Native scroll-snap is applied for you; keep overflow: auto on the viewport (default from getViewportProps).
  • For full-screen feeds, ensure html, body, #root stretch to 100%.

API reference

<SwipeDeck> props

Extends SwipeDeckOptions<T> plus:

  • as: custom element/component for the viewport (default "div").
  • children(context): render prop; receives { item, index, isActive, shouldPreload, props }.
  • className, style: forwarded to the viewport.
  • ref: imperative handle (SwipeDeckHandle) with prev, next, scrollTo, getState, getMotion.

SwipeDeckOptions<T>

  • items (required): readonly array of items.
  • orientation: "vertical" (default) | "horizontal".
  • direction: "ltr" (default) | "rtl" (affects horizontal gestures/wheel/keyboard).
  • defaultIndex: initial index for uncontrolled mode (default 0).
  • index: controlled index. When set, you must manage updates via onIndexChange.
  • onIndexChange(index, source): fires on any navigation. source is one of "user:gesture" | "user:wheel" | "user:keyboard" | "programmatic" | "visibility" | "snap".
  • onMotionChange(motion): animation-frame-throttled continuous motion callback. It does not put per-pixel motion into React render state.
  • loop: wrap navigation at ends (default false).
  • gesture: { threshold, flickVelocity, lockAxis, ignoreWhileAnimating } (defaults 10, 0.1, true, true).
  • wheel: { discretePaging, threshold, debounce, cooldown } (defaults true, 100, 120ms, 800ms) with aggressive dampening to prevent multi-item jumps.
  • keyboard: { enabled, global, bindings } (default enabled, scoped to viewport). Bindings default to arrows + Home/End + Page keys; RTL flips horizontal intent.
  • keyboardNavigation: boolean shorthand to disable keyboard entirely (default true).
  • virtual: { overscan, estimatedSize, getItemKey } (defaults 5, auto-measured viewport size, index key). Backed by @tanstack/react-virtual.
  • endReachedThreshold: number of items from ends to trigger onEndReached (default 3).
  • onEndReached({ distanceFromEnd, direction }): called when within threshold from start or end.
  • ariaLabel: label for the feed (default "Swipe feed").
  • visibility: { strategy, intersectionRatio, debounce }. strategy defaults to "position"; "intersection" activates an item after it crosses intersectionRatio (default 0.6) and falls back to position when IntersectionObserver is unavailable.
  • onItemActive(item, index), onItemInactive(item, index): exact lifecycle transitions for the active item. The initial item activates on mount and the final item becomes inactive on unmount.

Render context (children)

  • item: the data item.
  • index: item index.
  • isActive: whether the item is currently centered/active.
  • shouldPreload: whether the item is in the configured forward/backward preload window.
  • props: spread onto your item content (data and accessibility attributes; SwipeDeck's outer article owns its internal ref and positioning styles).

Imperative handle (SwipeDeckHandle)

  • prev(), next()
  • scrollTo(index, { behavior })
  • getState(): { index, isAnimating, canPrev, canNext }
  • getMotion(): latest SwipeDeckMotion snapshot without subscribing React render state.

useSwipeDeck return shape

  • State: index, isAnimating, canPrev, canNext, items, orientation.
  • Actions: prev(), next(), scrollTo(index, { behavior }).
  • Motion: getMotion() for the latest snapshot; use onMotionChange to subscribe.
  • Layout: virtualItems (offset/size/key/measureElement), totalSize.
  • Props helpers: getViewportProps(), getItemProps(index).

Continuous motion

onMotionChange emits { scrollOffset, viewportSize, position, index, offset, offsetRatio, direction, isSettled }. position is fractional, while index is the nearest item. offset is the nearest item's pixel displacement from its settled position, which makes a shared media layer follow the outgoing and incoming cards without reading the viewport DOM directly.

The callback is throttled with requestAnimationFrame. Keep high-frequency visual work imperative (for example, update a transform); only copy coarse changes such as motion.index into React state.

Helper hook

  • useWindowSize(): throttled window dimensions { width, height }.

Virtualization behavior

  • Always uses @tanstack/react-virtual under the hood for consistent offsets, even for small lists.
  • If virtual.estimatedSize is omitted, the viewport is measured via ResizeObserver and used as the estimate, keeping slides at viewport size by default.
  • virtual.getItemKey lets you provide stable keys (otherwise the index is used).
  • virtual.overscan tunes how many items render around the viewport (default 5).

Input handling

  • Gestures: pointer drag + flick with threshold/velocity detection, axis locking, and optional ignore-during-animation.
  • Wheel: discrete paging with direction locking, debounce, cooldown, and delta caps to prevent multi-item jumps on trackpads.
  • Keyboard: arrow/Home/End/PageUp/PageDown; can be global or scoped to the viewport; RTL-aware for horizontal decks.
  • Programmatic: scrollTo chooses smooth vs instant based on prefers-reduced-motion, and temporarily disables snap while animating to avoid fights with CSS snap.

Accessibility

  • Viewport: role="feed", aria-label, aria-busy while animating, focusable (tabIndex=0), native scroll-snap, touch-action set per axis.
  • Items: role="article" with aria-label that includes index/length; snap alignment applied on each item.
  • RTL-aware keyboard/wheel for horizontal feeds.

End reached & looping

  • onEndReached fires both near the start and near the end when within endReachedThreshold items.
  • loop wraps navigation for gestures, wheel, keyboard, and programmatic calls.

Example app

A TikTok-style vertical feed using YouTube iframes lives in example/.

bun install            # installs root + workspace deps
bun run build          # optional: build the library once
cd example
bun run dev            # starts Vite on http://localhost:5173

Key bits: render prop usage, shared-player synchronization through onMotionChange, global keyboard navigation, gesture swipe, mute button overlay, and custom chrome.

Scripts (root)

  • bun run build – bundle src/ to dist/ with sourcemaps.
  • bun run docs:api – generate markdown API docs in docs/api.
  • bun run docs:site – generate HTML TypeDoc site to site/docs (default theme).
  • bun run example:build:site – build the Vite example with base=/example/.
  • bun run site:prepare – rebuild docs+example into site/ for GitHub Pages.
  • bun run test – Vitest (jsdom) unit suite.
  • bun run test:browser – Vitest browser runner (wheel/scroll integration).
  • bun run test:example:live -- http://127.0.0.1:<port>/ – against an already-running example, verify every catalog video is embeddable, portrait, playing after unmute, and loaded through one stable iframe. Choose an unused local port; this network-dependent release check is intentionally separate from deterministic CI.
  • bun run test:coverage, bun run test:browser:coverage, bun run coverage:merge – coverage reports/merge.
  • bun run lint / bun run format – Biome lint/format.
  • bun run typecheck – TypeScript.
  • bun run storybook / bun run storybook:build – Storybook dev/build.

GitHub Pages (docs + example)

  • Build locally: bun run site:prepare (produces site/docs for API HTML, site/example for the Vite demo).
  • Deploy via CI: push to master and the Pages workflow builds both sections and publishes site/ to the gh-pages branch (docs at /docs/, example at /swipefeed/example/ by default).
  • If you fork/rename the repo or use a custom Pages root, set VITE_BASE=/<your-repo>/example/ when running example:build:site or adjust the workflow accordingly.

Notes & limitations

  • Ensure your viewport has a defined size; virtualization, motion snapshots, and snap rely on it.

About

React Tiktok Component

Resources

Contributing

Stars

2 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages