Overview
The project uses Motion Mini (motion/mini) as its animation engine, decoupled from the component interactivity layer.
All core animation consumers import from src/lib/motion.ts, not directly from motion/mini. The wrapper provides a reduced-motion guard that covers WAAPI animations (the existing CSS rule in base.css only covers CSS animations).
Scroll-Reveal Utility
The primary API is revealOnScroll() from src/lib/motion-reveal.ts. It wires IntersectionObserver to Motion Mini’s animate() for viewport-triggered animations.
import { revealOnScroll } from "@/lib/motion-reveal";
// Named preset
revealOnScroll(".cards", "fade-up");
// With options
revealOnScroll(".cards", "fade-up", {
duration: 0.8,
stagger: 0.1,
threshold: 0.2,
});
// Raw keyframes
revealOnScroll(".hero-element", {
opacity: [0, 1],
transform: ["rotate(-5deg) scale(0.8)", "rotate(0deg) scale(1)"],
});
Available Presets
Preset names are a stable API contract.
| Preset | Effect |
|---|---|
fade-in | Opacity 0 to 1 |
fade-up | Fade + slide up from below |
fade-down | Fade + slide down from above |
fade-left | Fade + slide from the right |
fade-right | Fade + slide from the left |
slide-up | Slide up 40px (no fade) |
slide-down | Slide down 40px (no fade) |
slide-left | Slide from right 40px (no fade) |
slide-right | Slide from left 40px (no fade) |
scale-in | Fade + scale from 0.9 |
scale-up | Fade + scale from 0.8 |
Child sites can pass additional presets at call time:
revealOnScroll(".element", "bounce-in", {
presets: {
"bounce-in": {
keyframes: { opacity: [0, 1], transform: ["scale(0.5)", "scale(1)"] },
duration: 0.6,
ease: "backOut",
},
},
});
Options
| Option | Type | Default | Description |
|---|---|---|---|
once | boolean | true | Animate only on first intersection |
threshold | number | 0.1 | IntersectionObserver threshold (0–1) |
rootMargin | string | "0px" | IntersectionObserver root margin |
duration | number | 0.5 | Duration in seconds (overrides preset) |
delay | number | 0 | Delay in seconds |
ease | Easing | "easeOut" | Motion easing function |
stagger | number | — | Stagger interval in seconds per element |
presets | Record | — | Additional presets for this call |
Direct animate() Usage
For animations beyond scroll-reveal, import animate directly from the wrapper:
import { animate, stagger } from "@/lib/motion";
// Animate specific elements
animate(".logo", { scale: [1, 1.1, 1] }, { duration: 0.3 });
// With stagger delay
animate(".nav-item", { opacity: [0, 1] }, { delay: stagger(0.05) });
Reduced Motion
Two complementary layers ensure accessibility:
- CSS layer —
base.cssforcesanimation-duration: 0.01mson all elements whenprefers-reduced-motion: reduceis active. This covers CSS animations and transitions. - JS layer — The
motion.tswrapper checksprefers-reduced-motionat eachanimate()call. When active, it setsduration: 0so elements reach their final state instantly. The check runs at call time, so runtime preference changes are respected.
View Transitions
The revealOnScroll utility manages its own lifecycle for View Transitions (ClientRouter). On astro:page-load, it disconnects stale observers and re-applies all registrations against the new DOM. Callers do not need to re-invoke after a page swap.
Direct animate() callers manage their own lifecycle. Use astro:before-swap for cleanup and astro:page-load for re-initialization, following the same pattern as other project modules.
Progressive Enhancement
Elements start visible — no CSS pre-hide state. Users with JS disabled see all content normally. Elements already in the viewport when revealOnScroll is called are marked as revealed and skip animation, preventing a visible-then-invisible flash.
Upgrade Path
The motion npm package is installed in core. Child sites can import from deeper paths without adding a dependency:
motion/mini— WAAPI-only engine (~2.3 KB)motion— full hybrid JS engine withinView,stagger,scroll(~17 KB)motion/react— React component API for React islands