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;
}
/* ... */
}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).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.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.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.animation shorthand resets animation-timeline and animation-range; a
kernel prints a shorthand before its longhands, so the timeline set beside it wins.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).reveal.preset: unexpected "fade-out" in "fade-out", expected fade-in | ..., reveal.range: unexpected "sideways" in "entry 0% sideways".css.properties.animation-timeline and
animation-range). Firefox has them behind a flag only.@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.range().motion() would: nothing stays
hidden.bun run test:browsers.reveal().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-coreIn 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.