Motion
Urbic’s motion layer is source, not a package. src/styles/motion/ is a dependency-free port of tailwind-animations (MIT, © Miguel Ángel Durán and community), adapted to Tailwind v4’s CSS-first shape and extended with a scroll-driven set the upstream does not have.
Class names match the upstream website, so its documentation applies one-to-one.
What is in it
98 animations. Ninety-six --animate-* shorthands in motion/index.css, plus the two marquee animations that live in tailwind-theme.css beside the primitive that uses them. Each becomes a utility: --animate-fade-in-up is animate-fade-in-up.
The catalog covers entrances and exits (fade, slide, zoom, roll, flip, swivel, wipe), attention-seekers (shake, jelly, tada, heartbeat, rubber-band, wobble, jiggle), rotations and spins, expansions and contractions, and an owned scroll-driven set: progress, parallax-up, parallax-down, parallax-left, parallax-settle, ken-burns, fade-through, recede, mask-up and the four wipe-in-*.
27 modifier utilities tune any of them: animate-duration-*, animate-delay-*, animate-ease* and animate-bezier-*, animate-iteration-count-*, animate-fill-mode-*, animate-steps-*, animate-direction-*, animate-play-running / animate-play-paused, plus the timeline family below.
<div class="animate-fade-in-up animate-duration-700 animate-delay-200">…</div>
The delay tokens use a tw-anim- prefix so they do not clash with Tailwind’s own transition delay-*.
Three upstream changes are worth knowing: --animate-pulse was dropped as identical to Tailwind’s built-in animate-pulse; the reduced-motion guard was added (the upstream ships none); and the scroll-driven extension is entirely this project’s.
Scroll-driven timelines
timeline-* sets animation-timeline, and scroll-timeline-axis-* and view-timeline-axis-* set the axis. Beyond those, the named timeline utilities are this project’s own and let one element’s animation be driven by another element’s scroll or visibility:
Declare on the tracked element with view-timeline-name-[--article], hoist it with timeline-scope-[--article] on a common ancestor, then consume it anywhere in that scope with timeline-[--article]. The canonical use is a fixed reading-progress bar tracking an article.
animate-range-* sets animation-range — worth reaching for when an animation at the bottom of a long document needs to scrub on the entry phase rather than the default.
The parallax pair
parallax-up / parallax-down drift a child by ±--parallax-shift of its own height. The parallax-child utility is the other half: it sizes and centres that child so it still covers its clipping frame at both extremes, deriving both numbers from the same knob. parallax-child-x is the horizontal twin for parallax-left.
They live next to the keyframes they must agree with rather than as calc()s at call sites, because split across two files the pair silently drifts and the failure is a sliver of background at one edge, visible at only one end of the scroll.
The size is the exact minimum, 100% / (1 - 2s), not a round over-provision — and that matters visually. object-cover pays for vertical oversize in horizontal crop, so every surplus point of height quietly eats the sides of the picture. At a 5% shift the exact size is 111% and costs 10% of the width; the tidy-looking 100% + 4s is 120% and costs 17% for the same travel, which was enough to clip a composition at both edges.
split-mask is the same kind of derived pair for mask-up: the clipping wrapper a unit rises into. Its padding is what stops overflow: clip shaving descenders at rest — a line box is not a glyph box, and at line-height: 1 a serif’s tails sit below it — and a negative margin keeps that padding off the layout so masking a heading does not move it. It uses clip rather than hidden because a hidden box is a scroll container, which would capture the view() timeline declared on the ancestor.
The two guards
Reduced motion. A global @media (prefers-reduced-motion: reduce) block sets near-zero — not none — durations, so animationend and transitionend listeners still fire, and neutralizes the scroll-behavior: smooth set on <html>. This is the single global source; individual primitives keep their motion-reduce: classes for clarity. It is honoured regardless of the useAnimations setting.
Unsupported timelines. Where scroll timelines are unsupported, an animation-timeline declaration is dropped and the animation runs once, time-based. That is fine for entrances — they end at identity, so content ends visible, which is <Reveal>’s documented degradation. It is not fine for scroll-only shapes: a played-once parallax leaves content offset, and fade-through ends invisible. So a @supports not (animation-timeline: view()) block makes those seven inert instead. progress is exempt, because its identity end state (a full bar) is exactly what no animation would render anyway.
If you add a scroll-only animation, add it to that list. The match is a substring match so variant-prefixed uses like md:animate-parallax-up are covered; the ceiling is a false positive on any future class containing one of those names.
The zero-JS scroll primitives
<Reveal> and <SplitFlip> in src/components/ui/ are scroll-driven entrance primitives with no JavaScript — they declare a view() timeline and let the browser scrub it. SplitFlip splits an authored headline on \n, which is why several config headlines carry explicit line breaks: a serif headline that re-wraps mid-phrase loses its composition.
A staggered reveal wants one named timeline rather than one per unit, which is what the named-timeline utilities above are for.
Turning it off
siteSettings.json.ts has useAnimations. Off, the decorative motion layer — scroll reveals and the rest — stops. useSmoothScroll is separate and controls Lenis; off, BaseLayout emits no script tag at all, so no page fetches it.
Neither switch affects prefers-reduced-motion, which is always honoured.
The stylesheet cost
The motion catalog is why deleting /examples/ui saves so much CSS: the dev catalog demos nearly the whole library, so its @keyframes count in the shared stylesheet drops from 108 to 44 when it goes. See Layout.