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

@ouvarov/scroll-parallax

v0.8.1

Published

Scroll parallax for React, under 1 KB per component. Translate, opacity, scale, rotate, stagger, plus FadeOnView, RevealOnView, ScrollProgress and StickyShrink. Zero JS at runtime. Pure CSS animation-timeline: view() / scroll().

Readme

@ouvarov/scroll-parallax

npm version bundle size types license downloads

One React component for scroll-driven animation. Translate, opacity, scale, rotate — all four scroll-animatable CSS properties in one component, under 1 KB gzipped per imported component. Plus <FadeOnView> and <RevealOnView>, one-tag wrappers for the common reveal-on-scroll cases, <ScrollProgress>, a page-level reading-progress bar, and <StickyShrink>, a sticky header that shrinks on scroll. The animation itself is pure CSS animation-timeline: view() (or scroll() for the page-level ones).

📺 Live demo & docs: https://ouvarov.github.io/scroll-parallax/

JS:   2.9 KB raw  /  1.2 KB gzipped  (all components; tree-shakes to 268–869 B per component)
CSS:  8.3 KB raw  /  1.9 KB gzipped

Install

npm i @ouvarov/scroll-parallax

Use

import { Parallax } from "@ouvarov/scroll-parallax";

export function Hero() {
  return (
    <Parallax amplitude={30}>
      <img src="/hero.jpg" alt="" />
    </Parallax>
  );
}

The CSS is auto-imported by the component — one line is all you need. The image drifts from +30px to -30px on the Y axis as it passes through the viewport. No useEffect, no IntersectionObserver, no scroll listener.

Custom start and end

<Parallax from={40} to={-20}>
  <img src="..." />
</Parallax>

Pulse — three-stop animation

Pass mid to drift in, then back out. Same trick for opacity (opacityMid).

<Parallax mid={0} from={40} to={40} opacityFrom={0} opacityMid={1} opacityTo={0}>
  <Card />
</Parallax>

Card translates from +40px to 0 then back to +40px, fading in at the apex and out again — a classic "rise and settle" pulse.

Scale and rotate

<Parallax amplitude={0} scaleFrom={0.8} scaleTo={1} rotateFrom={-4} rotateTo={0}>
  <img src="..." />
</Parallax>

rotateFrom / rotateTo take degrees as numbers. Scale is a unitless multiplier (1 = original size).

Opacity ramp

<Parallax amplitude={0} opacityFrom={0.2} opacityTo={1}>
  <img src="..." />
</Parallax>

Stagger — cascade across children

Set stagger and <Parallax> stops animating itself — instead each direct child runs the animation in a cascade. No mapping over children, no JS: each child's animation-range-start is shifted by an :nth-child rule.

<Parallax stagger={6} range="cover 0% cover 50%"
          from={40} to={0} opacityFrom={0} opacityTo={1}>
  {items.map((i) => <Card key={i} />)}
</Parallax>

range is the cascade window — you decide where the reveal lands. The cascade offsets each child's start inside it; the end is shared, so every child finishes at the same point. cover 50% is the viewport center, so the group is fully revealed by the time it's in the reading zone — not only at the very top (cover 100%). With stagger, give range in the explicit "<name> n% <name> n%" form (default cover 0% cover 50%).

The cascade unit is a percentage of view progress per child, not milliseconds — scroll-driven animations ignore time-based animation-delay. Child n starts at from + (n−1) × stagger%. Works best on a group that enters together (a row or grid). Pure CSS, capped at 24 children (beyond that they share the last offset); keep from + stagger × count under the range end.

Compose everything

<Parallax
  amplitude={80}
  axis="y"
  opacityFrom={0.3}
  opacityTo={1}
  scaleFrom={0.95}
  scaleTo={1}
  range="entry 0% cover 60%"
  easing="cubic-bezier(0.7, 0, 0.3, 1)"
>
  <img src="..." />
</Parallax>

FadeOnView — the common case

Fade in as the element enters the viewport. No opacity props, no range to remember.

import { FadeOnView } from "@ouvarov/scroll-parallax";

<FadeOnView>
  <Card />
</FadeOnView>

This is exactly <Parallax opacityFrom={0} opacityTo={1} range="entry">. Pass rise for an upward drift on the way in:

<FadeOnView rise={24}>
  <Card />
</FadeOnView>

The card starts 24px lower and settles as it fades. rise={0} (the default) is a pure fade.

RevealOnView — animate in at a threshold

The element stays hidden until it reaches a trigger line on the way up — by default the lower third — then plays a short ease-out reveal and holds. Where FadeOnView ramps across the whole entry, RevealOnView waits at the threshold, then reveals over a brief span.

import { RevealOnView } from "@ouvarov/scroll-parallax";

<RevealOnView>            {/* opacity 0 → 1, starting at 30% up */}
  <Card />
</RevealOnView>

<RevealOnView effect="scale">   {/* scale 0 → 1 */}
  <Card />
</RevealOnView>

<RevealOnView effect="both" threshold={0.5} span={0.1}>   {/* snappy grow + fade at center */}
  <Card />
</RevealOnView>

threshold={0.3} with span={0.15} maps to range="cover 30% cover 45%" — invisible below 30% of the pass, animating in across the next 15%, solid after. Shrink span for a snappier entrance.

It's still a scroll-driven animation: at normal scroll speed it reads as an entrance, but scrubbing slowly tracks the scroll, and it's reversible (scroll back up past the line and it hides again). A time-based one-shot would need JS, which this package deliberately avoids.

ScrollProgress — page reading bar

A fixed bar that fills as the whole page scrolls. Drop one tag at the root of your app — no scroll listener, no useState. Unlike the rest of the library it tracks the document with animation-timeline: scroll(), not a single element's view().

import { ScrollProgress } from "@ouvarov/scroll-parallax";

<ScrollProgress />                                  {/* 4px bar at the top, currentColor */}

<ScrollProgress height={6} color="#7c3aed" />       {/* thicker, accent color */}

<ScrollProgress position="bottom" />                {/* pin to the bottom edge */}

The fill is a composite-only scaleX, so the bar rides the compositor and never lays out or paints while you scroll. It stays active under prefers-reduced-motion — it mirrors the native scrollbar rather than adding decorative motion.

StickyShrink — header that shrinks on scroll

A position: sticky header that shrinks from a tall variant to a compact one over the first distance px of page scroll. No scroll listener, no useScroll.

import { StickyShrink } from "@ouvarov/scroll-parallax";

<StickyShrink>                              {/* 80px → 56px over 200px of scroll */}
  <Logo />
  <Nav />
</StickyShrink>

<StickyShrink from={120} to={64} distance={300}>
  <Header />
</StickyShrink>

It's driven by a single registered progress variable, --ouvarov-sticky-progress (0 → 1), animated on animation-timeline: scroll(root block). Children inherit it, so they can react to the same scroll without any extra wiring:

<StickyShrink>
  {/* logo scales down as the header collapses */}
  <img style={{ scale: "calc(1 - 0.25 * var(--ouvarov-sticky-progress))" }} />
</StickyShrink>

Unlike the parallax components, shrinking a header reflows the page below it, so this one does touch layout — bounded to one element and its subtree, only while scrolling the first distance px. Under prefers-reduced-motion it stays at its tall size and never shrinks.

Props

<Parallax>

| prop | type | default | notes | |------|------|---------|-------| | amplitude | number | 30 | Symmetric drift shortcut. Animates from +amplitude to -amplitude. Overridden by from/to. | | from | number | — | Start translate in px. | | mid | number | (from + to) / 2 | Middle translate in px (50% keyframe). Set explicitly to create a pulse / three-stop drift. | | to | number | — | End translate in px. | | axis | 'x' \| 'y' | 'y' | Which axis to drift on. | | opacityFrom | number | 1 | Start opacity (0–1). | | opacityMid | number | (opacityFrom + opacityTo) / 2 | Middle opacity (50% keyframe). Set explicitly for fade-in-then-out pulses. | | opacityTo | number | 1 | End opacity (0–1). | | scaleFrom | number | 1 | Start scale multiplier. | | scaleTo | number | 1 | End scale multiplier. | | rotateFrom | number | 0 | Start rotation in degrees. | | rotateTo | number | 0 | End rotation in degrees. | | stagger | number | — | When set, the wrapper isn't animated — each direct child runs the animation, offset by this percentage of view progress per child. range sets the window; children share its end. Pure CSS via :nth-child, capped at 24 children. | | range | string | 'cover 0% cover 100%'('cover 0% cover 50%' with stagger) | Any valid animation-range — where in the scroll the animation plays. With stagger, it's the cascade window (use the "<name> n% <name> n%" form); cover 50% ends at the viewport center. | | easing | string | 'linear' | Any valid animation-timing-function. | | className | string | — | Merged onto the wrapper. | | style | CSSProperties | — | Merged onto the wrapper. | | children | ReactNode | — | Whatever you want to animate. |

<FadeOnView>

Sugar over <Parallax>. Forwards className, style and easing unchanged.

| prop | type | default | notes | |------|------|---------|-------| | rise | number | 0 | Upward drift in px on the way in. 0 is a pure fade. | | range | string | 'entry' | Where the fade plays — defaults to the element's entry into the viewport. |

<RevealOnView>

Sugar over <Parallax>. Forwards className and style unchanged.

| prop | type | default | notes | |------|------|---------|-------| | effect | 'fade' \| 'scale' \| 'both' | 'fade' | fade ramps opacity 0→1, scale grows from 0→1, both combines them. Eases in with ease-out. | | threshold | number | 0.3 | Trigger line — fraction of the element's pass where the reveal starts. Below it, the element is hidden. Clamped to 0–1. | | span | number | 0.15 | How much scroll the reveal takes after the threshold. Together: range="cover {threshold×100}% cover {(threshold+span)×100}%". Smaller = snappier. |

<ScrollProgress>

Standalone page-level bar — renders its own fixed <div>, takes no children.

| prop | type | default | notes | |------|------|---------|-------| | height | number \| string | 4 | Bar thickness. Number → px; string used as-is. | | color | string | currentColor | Fill color. | | position | 'top' \| 'bottom' | 'top' | Which viewport edge to pin to. | | zIndex | number | 2147483647 | Stacking order — defaults high so it sits above page chrome. | | className | string | — | Merged onto the bar. | | style | CSSProperties | — | Merged onto the bar. |

<StickyShrink>

A position: sticky header. Exposes --ouvarov-sticky-progress (0 → 1) to its subtree.

| prop | type | default | notes | |------|------|---------|-------| | from | number \| string | 80 | Tall height before scroll. Number → px; string used as-is. | | to | number \| string | 56 | Compact height after shrinking. Number → px; string used as-is. | | distance | number \| string | 200 | Scroll distance from the top over which it shrinks. Number → px. | | className | string | — | Merged onto the header. | | style | CSSProperties | — | Merged onto the header. | | children | ReactNode | — | Header content. Read var(--ouvarov-sticky-progress) to react to the shrink. |

The component renders a <div> wrapper. The wrapper is the animated element.

Each <Parallax> instance is scoped — values don't bleed across siblings.

Browser support

| Browser | Status | |---------|--------| | Chrome / Edge / Opera | ✅ 115+ (Aug 2023) | | Safari | ✅ 18+ (Sept 2024) | | Firefox | ⚠ Behind layout.css.scroll-driven-animations.enabled flag | | iOS Safari | ✅ 18+ | | Older browsers | Element renders normally, no animation |

Reduced motion is respected automatically via @media (prefers-reduced-motion: reduce).

Why this exists

animation-timeline: view() has been stable in Chromium since 2023 and Safari since 2024 — the browser reads scroll progress natively, no JavaScript needed. This package is a typed React wrapper over that native CSS API.

Use without React

The React components do one thing: render an element with a class and a few CSS custom properties. All the animation lives in style.css. So you don't actually need React — import the stylesheet, add the class, set the properties. The same classes work in Vue, Svelte, Astro, or plain HTML.

Via a bundler:

import "@ouvarov/scroll-parallax/style.css";

Or in plain HTML, straight from a CDN:

<link rel="stylesheet" href="https://unpkg.com/@ouvarov/scroll-parallax/style.css">

Then use the classes directly — no build step, no JavaScript:

<!-- drifts +60px → -60px as it crosses the viewport -->
<div class="ouvarov-parallax" style="--ouvarov-parallax-from: 60px; --ouvarov-parallax-to: -60px">
  <img src="/hero.jpg" alt="">
</div>

<!-- bare class uses the defaults (+30px → -30px) -->
<div class="ouvarov-parallax"><img src="/hero.jpg" alt=""></div>

<!-- horizontal drift -->
<div class="ouvarov-parallax" data-axis="x">…</div>

<!-- page reading bar, pinned to the top -->
<div class="ouvarov-scroll-progress" data-position="top"></div>

<!-- sticky header that shrinks 80px → 56px over 200px of scroll -->
<header class="ouvarov-sticky-shrink">…</header>

These are the same class names and --ouvarov-* custom properties the React components emit — read style.css for the full set. The React layer is just typed sugar that sets them for you.

Scope

One thing, done well: translate, opacity, scale and rotate on scroll. No spring physics, no gesture handling, no layout animations — by design.

  • The <Parallax> wrapper adds one DOM node. Don't want it? Skip the component and apply the .ouvarov-parallax class with the custom properties directly — the animation is pure CSS, no React required (see Use without React).

License

MIT © Olexandr Uvarov