DevTools Logo
All posts

Modern CSS Motion: Springs, Scrolls, Views & Contrast

September 5, 2026 · DevTools

css
animation
accessibility
web-vitals
ux

Motion makes interfaces feel alive, but shipping it means solving physics, perception, performance, and transitions together. These five tools cover the loop: the CSS Spring Physics Generator turns stiffness and damping into easings, the APCA Contrast Calculator scores animated text as eyes see it, the Web Vitals Budget Simulator keeps LCP, INP, and CLS in budget, the CSS View Transitions Builder generates page transitions, and CSS Scroll-Driven Animations wires motion to scroll timelines.

From stiffness and damping to cubic-bezier

The spring generator models a mass/spring/damper system through springValue(time, parameters) with stiffness, damping, mass, and velocity. Frequency comes from wn = sqrt(k / m) and the damping ratio from zeta = c / (2 * sqrt(k * m)): underdamped oscillates, critically damped settles fastest, overdamped creeps to rest. generateSpring() returns the ratio, 31 samples, and settleTime() — when the response stays within 1% of rest.

CSS cannot run spring integrators, so approximateCubicBezier() fits the curve: x points stay fixed at 0.25, 0.75 while coordinate descent tunes y1 and y2 over 8 passes against the samples. The result ships as CSS plus a fit-error score:

/* stiffness 170, damping 14, mass 1 → underdamped, settles in ~0.9s */
.card-enter {
  transition-timing-function: cubic-bezier(0.25, 0.42, 0.75, 0.95);
  transition-duration: 900ms;
}

Use light damping for playful overshoot on toasts and badges, and near-critical damping for snappy dialogs and menus.

Contrast your eyes actually see, budgets you can keep

APCA (0.1.9) replaces the WCAG 2 ratio with a perceptual lightness score Lc (0–108). Channels are linearized with a 2.4 gamma, weighted 0.2126729/0.7151522/0.072175, soft-clamped near black, then scored with different exponents for dark-on-light versus light-on-dark. getApcaResult() also reports the legacy ratio, and getContrastRecommendations() grades bronze, silver, and gold:

TextMinimum Lc (bronze / silver / gold)
Body text (16px, regular)60 / 75 / 90
Large or bold (≥24px, or ≥18px at 700+)45 / 60 / 75

Animated text that dips below bronze mid-fade is unreadable no matter how pretty the easing — check endpoints with analyzeApca() before you ship.

Then protect the experience with budgets. calculateWebVitals() models a 10 Mbps link, sums render-blocking transfer into LCP, execution plus input delay into INP, and impact × shift fractions into CLS. Ratings follow Google thresholds — LCP good ≤ 2500ms / poor > 4000ms, INP good ≤ 200ms / poor > 500ms, CLS good ≤ 0.1 / poor > 0.25.

View transitions and scroll timelines without the boilerplate

The View Transitions API animates snapshots of the old and new page from a preset — fade, slide, scale, flip, or morph. buildViewTransitionCss() validates the transition name (default root), normalizes duration (350ms default), then generates paired @keyframes vt-{preset}-{suffix}-old/new — slide ±24px, scale 0.92/1.08, flip ±90° in 800px perspective, morph with a rounded clip — wired to ::view-transition-old(name) and ::view-transition-new(name).

Scroll-driven animations drop the scroll listener. buildScrollDrivenCss() offers progress-bar, reveal, parallax, and stagger, emitting animation-timeline: scroll(block) or view(block) plus animation-range from entry, entry-crossing, exit, or contain. Every block ships an @supports not (animation-timeline: scroll()) fallback that leaves content visible.

Try Them