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

@cssints/reveal

v0.3.1

Published

Scroll-driven reveal animations for cssints: motion's presets on animation-timeline: view(), with a tiny IntersectionObserver fallback

Readme

@cssints/reveal

Scroll-driven reveal animations for cssints: an element runs one of @cssints/motion's entrance presets as it scrolls into view, on animation-timeline: view() and animation-range, as one chain member of cssints/plugin scope(). In the spirit of the supersonic plugin and lax.js, with the browser doing the work. Where a target lacks scroll-driven animations, a 0.3 kB script plays the same keyframes once when the element enters.

import { cn, hover } from "cssints" with { type: "cssints" };
import { reveal } from "@cssints/reveal" with { type: "cssints" };

export const fade = reveal();
export const card = reveal()
  .preset("slide-in-up")
  .range("entry 0% entry 100%")
  .duration("500ms")
  .easing("linear");
export const tile = cn(reveal().preset("blur-in"), hover(reveal().duration("1s")));
@layer _.k {
  @keyframes kf-7f473efd {
    from {
      opacity: var(--motion-o);
      translate: var(--motion-x) var(--motion-y);
      scale: var(--motion-s);
      filter: blur(var(--motion-b));
    }
  }
  @keyframes kf-4f766ef7 {
    from {
      opacity: var(--motion-o);
    }
  }
  ._reveal-core {
    animation: var(--reveal-k) var(--reveal-d) var(--reveal-e) both;
    animation-range: var(--reveal-g);
    --reveal-k: kf-7f473efd;
    --reveal-rk: kf-4f766ef7;
    --reveal-d: 300ms;
    --reveal-e: ease-out;
    --reveal-g: entry 0% cover 40%;
    --motion-o: 0;
    --motion-x: 0;
    --motion-y: 0;
    --motion-s: 1;
    --motion-b: 0;
    animation-timeline: view();
  }
  @media (prefers-reduced-motion: reduce) {
    ._reveal-core {
      animation-name: var(--reveal-rk);
    }
  }
  /* only when a target lacks scroll-driven animations */
  @supports not (animation-timeline: view()) {
    [data-reveal] ._reveal-core:not([data-reveal-in]) {
      animation-play-state: paused;
    }
  }
}
@layer _.a {
  ._1a9p5a9 {
    --motion-y: 25%;
  }
  ._yorqor {
    --reveal-g: entry 0% entry 100%;
  }
  ._k4abtq {
    --reveal-d: 500ms;
  }
  /* ... */
}

Rules

  • Methods, each once, in any order, all optional: preset(name) (fade-in), range(<'animation-range'>) (entry 0% cover 40%), duration(<time [0s,∞]>) (300ms) and easing(<easing-function>) (ease-out). Each is one class that sets a custom property, so a method works under a condition. Where the timeline is native, the range decides when the animation runs and the scroll drives it (back and forth); the duration and the easing shape the fallback's one play (the easing applies to the scrubbed progress too).
  • Presets are motion's entrance presets: fade-in, slide-in-up, slide-in-down, slide-in-left, slide-in-right, scale-in, blur-in. The -out ones are not: the fallback can only play when an element enters.
  • Reuse of motion. The kernel names motion's motionKeyframes.in and motionKeyframes.reducedIn, which carries their rules (shared with motion() on the same page: one rule each), and a preset sets the values those keyframes read, from motion's presets table under motion's names (motionVars). Nothing is copied, not even a name.
  • Reduced motion. Under prefers-reduced-motion: reduce the kernel runs the opacity-only keyframes (motion's rule): a slide, scale or blur becomes a fade over the same range.
  • One kernel, written as styles. The animation shorthand resets animation-timeline and animation-range; a kernel prints a shorthand before its longhands, so the timeline set beside it wins.
  • Conflicts. reveal() sets the animation: a cn() that holds it and an animation* property is a build error, derived from the kernel: cssints: reveal: kernel core sets animation, animation-timeline, animation-range, animation-name (this style also sets animation-duration).
  • Errors are the grammar's, in the types and at build time: reveal.preset: unexpected "fade-out" in "fade-out", expected fade-in | ..., reveal.range: unexpected "sideways" in "entry 0% sideways".

Browsers

  • Native: Chrome 115, Safari 26 (scroll-driven animations, css.properties.animation-timeline and animation-range). Firefox has them behind a flag only.
  • The fallback. When a browserslist target lacks them, the build adds the @supports not rule above to the sheet (it stays out of the sheet for targets that all have them), and the kernel's animation-timeline and animation-range give no warning: the runtime polyfills both. The script, @cssints/reveal/runtime, comes with it: the runtime entry names it (script), so the module that uses reveal() imports it through virtual:cssints/runtime/reveal.io, which is empty when every target has the timeline. Nothing to import by hand. In the browser it does nothing where CSS.supports("animation-timeline:view()"); elsewhere it marks <html data-reveal> (the rule then holds each reveal at its first frame) and an IntersectionObserver gives an element data-reveal-in when it enters the viewport, which plays the keyframes once, over duration(). A MutationObserver picks up elements added later. 536 bytes minified, 319 gzip; no imports.
  • Why not a polyfill. flackr's scroll-timeline polyfill scrubs the animation as the native timeline does, but it is about 61 kB minified (17 kB gzip), rewrites stylesheets and runs on every scroll. A reveal is an entrance: playing it once on entry is what most sites want, costs 1/50 of the bytes and nothing per scroll. The price is that the fallback is not scrubbed and ignores range().
  • Without the script, a target that lacks the timeline plays the animation on load, as motion() would: nothing stays hidden.
  • Tested in Chrome 154 and WebKit 26.6 (native, and the fallback forced) and Firefox 155 (the fallback): bun run test:browsers.

Limits

  • With targets that mix both (the default browserslist), the script ships to every browser and only probes where the timeline is native.
  • The fallback does not follow the range, the scroll direction or an element that leaves the viewport: it plays once. An element that gets the class after it is in the document (not added with it) is not seen.
  • Until the script runs, a reveal below the fold may start playing; it is paused where it is and resumes on entry. The script runs with the first module that uses reveal().
  • Entrance presets only, one per element; for an exit on scroll write the animation with animationTimeline("view()") and animationRange("exit ...") yourself.
cd packages/reveal && bun run test            # fixtures through the engine (output by targets, runtime module, conflicts, errors)
cd packages/reveal && bun run test:browsers   # Chrome, Firefox and WebKit through playwright-core

Credits

In the spirit of the supersonic plugin (MIT, Alex Illarionov) and lax.js (MIT, Alex Fox). The presets are @cssints/motion's. No code is copied.