spring / stagger / observeEntry
Motion primitives that leave the drawing to you. A spring solved exactly (also as a CSS linear() easing), stagger delays, a one-shot viewport entry and the reader's reduced-motion preference. None of them owns an animation loop, so they drive Web Animations, CSS transitions or your own requestAnimationFrame equally.
API
spring(options?)
Returns { at, duration, easing }. at(seconds) is progress from 0 to 1 (an underdamped spring passes 1 before settling); duration is how long it takes to stay within 0.1% of the target; easing is the same curve as a CSS linear() string over that duration.
| Option | Description | Default |
|---|---|---|
stiffness |
Pull towards the target; higher is faster | 170 |
damping |
Resistance; below 2 * sqrt(stiffness * mass) it overshoots |
26 |
mass |
Inertia | 1 |
velocity |
Starting speed, in progress units per second | 0 |
stagger(index, count, options?)
Delay, in seconds, for item index of count, starting from the first item, the last or the centre (from).
observeEntry(element, options)
Calls onEnter once, when the element's top first passes line (a fraction of the viewport height from the top), then stops observing. The callback receives { reduced }: when it is true, show the end state without animating. Returns a function that stops observing early.
prefersReducedMotion()
true when the reader asked for reduced motion; false where there is no window (SSR, Node).
Example
import { observeEntry, prefersReducedMotion, spring, stagger } from 'ranuts';
const { duration, easing } = spring({ stiffness: 260, damping: 20 });
const cards = [...document.querySelectorAll('.card')];
cards.forEach((card, index) => {
observeEntry(card, {
line: 0.85,
onEnter: ({ reduced }) => {
if (reduced) return; // already in its final place
card.animate(
[
{ opacity: 0, transform: 'translateY(24px)' },
{ opacity: 1, transform: 'none' },
],
{
duration: duration * 1000,
delay: stagger(index, cards.length, { each: 0.06 }) * 1000,
easing,
fill: 'backwards',
},
);
},
});
});Notes
- Content stays visible without script. Animate from an offset with
fill: 'backwards'rather than hiding elements in CSS, so a failed script or reduced motion leaves everything in place. - Where IntersectionObserver is missing,
observeEntryenters at once instead of leaving the element waiting. - The spring is exact, not stepped, so the same options give the same curve at any frame rate.