npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

react-kino

v0.3.1

Published

Cinematic scroll-driven storytelling components for React

Downloads

756

Readme


Why react-kino

  • Tiny -- the core scroll engine is under 1 KB gzipped. GSAP ScrollTrigger alone is 33 KB.
  • Declarative -- compose <Scene>, <Reveal>, <ScrollTransform>, <Parallax>, <Counter>, <StickyHeader>, <Marquee>, and <TextReveal> like regular React components. No imperative timelines.
  • Lightweight runtime -- react-kino uses a tiny internal engine package (@react-kino/core) plus React peers.
  • SSR-safe -- every component renders children on the server and animates on the client.

Installation

npm install react-kino
pnpm add react-kino
bun add react-kino

Requirements: React 18+

Quick Start

import { Kino, Scene, Reveal, Counter } from "react-kino";

function App() {
  return (
    <Kino>
      {/* A pinned scene that spans 300vh of scroll distance */}
      <Scene duration="300vh">
        {(progress) => (
          <div style={{ height: "100vh", display: "grid", placeItems: "center" }}>
            <Reveal animation="fade-up" at={0}>
              <h1>Welcome</h1>
            </Reveal>

            <Reveal animation="scale" at={0.3}>
              <p>Scroll-driven storytelling, made simple.</p>
            </Reveal>

            <Reveal animation="fade" at={0.6}>
              <Counter from={0} to={10000} format={(n) => `${n.toLocaleString()}+ users`} />
            </Reveal>
          </div>
        )}
      </Scene>
    </Kino>
  );
}

That is a complete scroll experience: the section pins in place, content fades in at different scroll points, and a number counts up -- all in ~20 lines.


Components

<Kino>

Root provider that initializes the scroll tracking engine. Wrap your app or page layout.

import { Kino } from "react-kino";

<Kino>
  {/* your scenes and content */}
</Kino>

| Prop | Type | Default | Description | |------|------|---------|-------------| | children | ReactNode | -- | Child elements |


<Scene>

A pinned scroll section. Content stays fixed in the viewport while the user scrolls through the scene's duration. This is the core building block.

import { Scene } from "react-kino";

{/* Static children -- use child components that read progress from context */}
<Scene duration="200vh">
  <MyAnimatedContent />
</Scene>

{/* Render prop -- get progress directly */}
<Scene duration="400vh">
  {(progress) => (
    <div style={{ opacity: progress }}>
      {Math.round(progress * 100)}% scrolled
    </div>
  )}
</Scene>

| Prop | Type | Default | Description | |------|------|---------|-------------| | duration | string | -- | Scroll distance the scene spans. Supports vh and px units (e.g. "200vh", "1500px") | | pin | boolean | true | Whether to pin (sticky) the inner content during scroll | | children | ReactNode \| (progress: number) => ReactNode | -- | Static content or render function receiving progress (0-1) | | className | string | -- | CSS class for the outer spacer element | | style | CSSProperties | -- | Inline styles for the sticky inner container |

Context: <Scene> provides a SceneContext that child components (<Reveal>, <Counter>, <CompareSlider>) automatically read from. You do not need to pass progress manually.


<Reveal>

Scroll-triggered entrance animation. Place inside a <Scene> or provide a progress prop directly.

import { Reveal } from "react-kino";

<Scene duration="300vh">
  <Reveal animation="fade-up" at={0.2}>
    <h2>Appears at 20% scroll</h2>
  </Reveal>

  <Reveal animation="blur" at={0.5} duration={800} delay={200}>
    <p>Blurs in at 50% with a delay</p>
  </Reveal>
</Scene>

| Prop | Type | Default | Description | |------|------|---------|-------------| | at | number | 0 | Progress value (0-1) when animation triggers | | animation | RevealAnimation | "fade" | Animation preset (see below) | | duration | number | 600 | Animation duration in milliseconds | | delay | number | 0 | Delay before animation starts in milliseconds | | progress | number | -- | Direct progress override (0-1). If omitted, reads from parent <Scene> context | | children | ReactNode | -- | Content to reveal | | className | string | -- | CSS class for the wrapper div |

Animation presets:

| Preset | Effect | |--------|--------| | "fade" | Opacity 0 to 1 | | "fade-up" | Fade in + slide up 40px | | "fade-down" | Fade in + slide down 40px | | "scale" | Fade in + scale from 0.9 to 1 | | "blur" | Fade in + unblur from 12px |


<Parallax>

A layer that scrolls at a different speed than the page, creating depth.

import { Parallax } from "react-kino";

{/* Background image scrolls at half speed */}
<Parallax speed={0.3}>
  <img src="/hero-bg.jpg" alt="" style={{ width: "100%", height: "120vh", objectFit: "cover" }} />
</Parallax>

{/* Foreground element scrolls faster */}
<Parallax speed={1.5}>
  <div className="floating-badge">New</div>
</Parallax>

| Prop | Type | Default | Description | |------|------|---------|-------------| | speed | number | 0.5 | Speed multiplier. 1 = normal scroll, < 1 = slower (background feel), > 1 = faster (foreground feel) | | direction | "vertical" \| "horizontal" | "vertical" | Scroll direction for the parallax offset | | children | ReactNode | -- | Content to apply parallax to | | className | string | -- | CSS class | | style | CSSProperties | -- | Inline styles (merged with transform) |


<Counter>

An animated number that counts between two values as the user scrolls. Automatically reads progress from a parent <Scene>.

import { Counter } from "react-kino";

<Scene duration="200vh">
  <Counter from={0} to={1000000} at={0.2} span={0.5} />

  <Counter
    from={0}
    to={99.9}
    format={(n) => `${n.toFixed(1)}%`}
    easing="ease-in-out"
  />
</Scene>

| Prop | Type | Default | Description | |------|------|---------|-------------| | from | number | -- | Starting value | | to | number | -- | Ending value | | at | number | 0 | Progress value (0-1) when counting begins | | span | number | 0.3 | How much of the progress range (0-1) the count spans | | format | (value: number) => string | toLocaleString | Formatting function for the displayed value | | easing | string \| (t: number) => number | "ease-out" | Easing preset name or custom easing function | | progress | number | -- | Direct progress override (0-1). If omitted, reads from parent <Scene> context | | className | string | -- | CSS class for the <span> element |


<ScrollTransform>

Interpolates CSS transforms and opacity between two states as the user scrolls. Perfect for 3D device tilts, slide-in effects, and any scroll-driven transform animation.

import { ScrollTransform } from "react-kino";

<Scene duration="350vh">
  <ScrollTransform
    from={{ rotateX: 40, rotateY: -12, scale: 0.82, opacity: 0.3 }}
    to={{ rotateX: 0, rotateY: 0, scale: 1, opacity: 1 }}
    perspective={1200}
    easing="ease-out-cubic"
    transformOrigin="center bottom"
  >
    <div className="card">Your content</div>
  </ScrollTransform>
</Scene>

| Prop | Type | Default | Description | |------|------|---------|-------------| | from | TransformState | -- | Starting transform state | | to | TransformState | -- | Ending transform state | | at | number | 0 | Progress value (0-1) when transform begins | | span | number | 1 | How much of the progress range the transform spans | | easing | string \| (t: number) => number | "ease-out" | Easing preset name or custom function | | perspective | number | -- | CSS perspective in px (enables 3D transforms) | | transformOrigin | string | "center center" | CSS transform-origin | | progress | number | -- | Direct progress override. If omitted, reads from parent <Scene> context | | className | string | -- | CSS class for the wrapper div | | style | CSSProperties | -- | Inline styles (merged with computed transform) |

TransformState properties: x, y, z (px), scale, scaleX, scaleY, rotate, rotateX, rotateY (deg), skewX, skewY (deg), opacity (0-1).


<CompareSlider>

A before/after comparison slider. Supports both drag interaction and scroll-driven modes.

import { CompareSlider } from "react-kino";

{/* Interactive drag mode */}
<CompareSlider
  before={<img src="/before.jpg" alt="Before" />}
  after={<img src="/after.jpg" alt="After" />}
/>

{/* Scroll-driven mode inside a Scene */}
<Scene duration="200vh">
  <CompareSlider
    scrollDriven
    before={<img src="/before.jpg" alt="Before" />}
    after={<img src="/after.jpg" alt="After" />}
  />
</Scene>

| Prop | Type | Default | Description | |------|------|---------|-------------| | before | ReactNode | -- | Content shown on the "before" side (always visible underneath) | | after | ReactNode | -- | Content shown on the "after" side (revealed via clip) | | scrollDriven | boolean | false | If true, slider position follows scroll progress instead of drag | | progress | number | -- | Progress override (0-1). When scrollDriven, defaults to parent <Scene> context | | initialPosition | number | 0.5 | Initial slider position (0-1) in drag mode | | className | string | -- | CSS class for the container |


<HorizontalScroll>

Converts vertical scroll into horizontal movement. Wrap <Panel> components inside it.

import { HorizontalScroll, Panel } from "react-kino";

<HorizontalScroll>
  <Panel>
    <div style={{ background: "#111", color: "#fff", padding: 60 }}>
      <h2>Panel One</h2>
    </div>
  </Panel>
  <Panel>
    <div style={{ background: "#222", color: "#fff", padding: 60 }}>
      <h2>Panel Two</h2>
    </div>
  </Panel>
</HorizontalScroll>

<HorizontalScroll> props:

| Prop | Type | Default | Description | |------|------|---------|-------------| | children | ReactNode | -- | <Panel> components | | className | string | -- | CSS class for the outer spacer | | panelHeight | string | "100vh" | Height of each panel as a CSS string |

<Panel> props:

| Prop | Type | Default | Description | |------|------|---------|-------------| | children | ReactNode | -- | Panel content | | className | string | -- | CSS class | | style | CSSProperties | -- | Inline styles (merged with default 100vw x 100vh sizing) |


<Progress>

A fixed scroll progress indicator. Supports bar, dots, and ring styles.

import { Progress } from "react-kino";

{/* Simple top bar */}
<Progress />

{/* Ring indicator in the corner */}
<Progress type="ring" position="bottom" color="#10b981" ringSize={40} />

{/* Dot pagination on the right */}
<Progress type="dots" position="right" dotCount={8} color="#fff" />

| Prop | Type | Default | Description | |------|------|---------|-------------| | type | "bar" \| "dots" \| "ring" | "bar" | Visual style of the indicator | | position | "top" \| "bottom" \| "left" \| "right" | "top" | Fixed position on screen | | color | string | "#3b82f6" | Color of the progress fill / active dots / ring stroke | | trackColor | string | "transparent" | Background / inactive color | | progress | number | -- | Progress override (0-1). If omitted, reads page scroll progress | | dotCount | number | 5 | Number of dots (only for "dots" type) | | ringSize | number | 48 | Diameter in pixels (only for "ring" type) | | className | string | -- | CSS class for the wrapper |


<VideoScroll>

Scrubs through a video as the user scrolls -- like the AirPods Pro / iPhone product pages.

import { VideoScroll } from "react-kino";

<VideoScroll src="/product.mp4" duration="400vh" poster="/poster.jpg">
  {(progress) => (
    <div style={{ position: "absolute", inset: 0, display: "grid", placeItems: "center" }}>
      <h2 style={{ opacity: progress, color: "#fff", fontSize: "4rem" }}>
        Scroll to reveal
      </h2>
    </div>
  )}
</VideoScroll>

| Prop | Type | Default | Description | |------|------|---------|-------------| | src | string | -- | URL of the video file (MP4 recommended, no audio needed) | | duration | string | "300vh" | Scroll distance the video scrubbing spans | | pin | boolean | true | Whether to pin the video while scrubbing | | poster | string | -- | Poster image shown before the video loads | | children | ReactNode \| (progress: number) => ReactNode | -- | Overlay content rendered on top of the video | | className | string | -- | CSS class for the outer spacer |


<StickyHeader>

A sticky navigation bar that transitions from transparent to a solid background with backdrop blur as the user scrolls past a threshold.

import { StickyHeader } from "react-kino";

<StickyHeader threshold={40} background="rgba(0, 0, 0, 0.72)" blur>
  <div style={{ maxWidth: 980, margin: "0 auto", height: 48, display: "flex", alignItems: "center", justifyContent: "space-between", padding: "0 24px" }}>
    <span>My Site</span>
    <nav>
      <a href="#features">Features</a>
      <a href="#pricing">Pricing</a>
    </nav>
  </div>
</StickyHeader>

| Prop | Type | Default | Description | |------|------|---------|-------------| | threshold | number | 80 | Scroll distance (px) before the header becomes solid | | background | string | "rgba(0,0,0,0.8)" | Background color when scrolled past threshold | | blur | boolean | true | Whether to apply backdrop blur when scrolled | | children | ReactNode | -- | Header content | | className | string | -- | CSS class | | style | CSSProperties | -- | Inline styles |


<Marquee>

An infinitely scrolling ticker. Items are automatically duplicated to create a seamless loop. Respects prefers-reduced-motion by falling back to a static flex layout.

import { Marquee } from "react-kino";

<Marquee speed={30} direction="left" pauseOnHover>
  <span>React</span>
  <span>TypeScript</span>
  <span>Next.js</span>
  <span>Tailwind</span>
</Marquee>

| Prop | Type | Default | Description | |------|------|---------|-------------| | speed | number | 40 | Speed in pixels per second | | direction | "left" \| "right" | "left" | Scroll direction | | pauseOnHover | boolean | true | Pause animation on hover | | gap | number | 32 | Gap between items in px | | children | ReactNode | -- | Items to scroll | | className | string | -- | CSS class |


<TextReveal>

Word-by-word, character-by-character, or line-by-line text reveal driven by scroll progress.

import { TextReveal } from "react-kino";

<Scene duration="300vh">
  {(progress) => (
    <TextReveal progress={progress} mode="word" at={0.1} span={0.7}>
      Scroll-driven storytelling components for React. Build cinematic experiences without the complexity.
    </TextReveal>
  )}
</Scene>

| Prop | Type | Default | Description | |------|------|---------|-------------| | children | string | -- | The text to reveal | | mode | "word" \| "char" \| "line" | "word" | How to split the text into tokens | | at | number | 0 | Progress value (0-1) when reveal starts | | span | number | 0.8 | How much of the progress range the full reveal spans | | color | string | currentColor | Color of revealed tokens | | dimColor | string | -- | Color of unrevealed tokens (default: 15% opacity) | | progress | number | -- | Direct progress override. If omitted, reads from parent <Scene> context | | className | string | -- | CSS class for the wrapper |


Hooks

useScrollProgress()

Returns the page-level scroll progress as a number from 0 to 1.

import { useScrollProgress } from "react-kino";

function ScrollPercentage() {
  const progress = useScrollProgress();
  return <div>{Math.round(progress * 100)}%</div>;
}

useSceneProgress(ref, durationPx)

Returns scene-level scroll progress for a specific element. Useful when building custom scroll-driven components outside of <Scene>.

import { useRef } from "react";
import { useSceneProgress } from "react-kino";

function CustomScene() {
  const ref = useRef<HTMLDivElement>(null);
  const progress = useSceneProgress(ref, 1500); // 1500px scroll distance

  return (
    <div ref={ref} style={{ height: 1500 }}>
      <div style={{ position: "sticky", top: 0 }}>
        Progress: {progress.toFixed(2)}
      </div>
    </div>
  );
}

useSceneContext()

Access the progress value from a parent <Scene>. Useful for building custom components that react to scene progress.

import { useSceneContext } from "react-kino";

function CustomFadeIn() {
  const { progress } = useSceneContext();
  return <div style={{ opacity: progress }}>I fade in as you scroll</div>;
}

useKino()

Access the root ScrollTracker instance from <Kino>. For advanced use cases where you need direct access to the scroll engine.

useIsClient()

SSR guard. Returns false on the server and during hydration, true after the component mounts on the client.


SSR / Next.js

react-kino is SSR-safe and defers scroll logic to useEffect.

Next.js App Router: Use react-kino inside a client component boundary ("use client").

// app/page.tsx
"use client";
import { Kino, Scene, Reveal } from "react-kino";

export default function Page() {
  return (
    <Kino>
      <Scene duration="200vh">
        <Reveal animation="fade-up">
          <h1>Works with App Router</h1>
        </Reveal>
      </Scene>
    </Kino>
  );
}

What happens on the server: Components render their children immediately with no animation styles. Scroll tracking starts after hydration on the client.


Accessibility

react-kino respects the prefers-reduced-motion media query:

  • <Reveal> -- content renders immediately in its visible state, no animation
  • <Parallax> -- parallax offset is disabled, content scrolls normally
  • <ScrollTransform> -- jumps to the to state immediately, no interpolation
  • <Counter> -- displays the final to value immediately once progress reaches at
  • <Marquee> -- renders items in a static flex layout instead of animating
  • <StickyHeader> -- transitions are disabled, background changes immediately

No additional configuration is required. This behavior is automatic.


Performance

  • Passive scroll listeners -- all scroll event listeners use { passive: true }
  • requestAnimationFrame batching -- scroll updates are batched via RAF to avoid layout thrashing
  • GPU-accelerated transforms -- parallax and reveal animations use transform and opacity (composite-only properties)
  • will-change hints -- applied to animating elements for browser optimization
  • Sub-1 KB core -- @react-kino/core contains all scroll math with zero dependencies
  • Tree-shakeable -- import only the components you use; unused code is eliminated at build time

Browser Support

| Feature | Chrome | Firefox | Safari | Edge | |---------|--------|---------|--------|------| | Core scroll tracking | 64+ | 60+ | 13+ | 79+ | | position: sticky | 56+ | 59+ | 13+ | 79+ | | prefers-reduced-motion | 74+ | 63+ | 10.1+ | 79+ |


License

MIT