Speed1×
Slow motion multiplier
Reduced motion
Show
Springs
JS mirror only — no CSS formpress
300ms · bounce 0.10spring —
pressCSS counterpart —
--duration-press + --ease-entryDuration
Bounce
swap
400ms · bounce 0.15spring —
swapCSS counterpart —
--duration-enter + --ease-moveDuration
Bounce
panel
500ms · bounce 0.20spring —
panelCSS counterpart —
--duration-overlay + --ease-drawerDuration
Bounce
layout
500ms · bounce 0.15spring —
layoutCSS counterpart —
--duration-overlay + --ease-moveDuration
Bounce
What the preview actually is
honest about the modelMotion describes a spring as duration and bounce. This lab reproduces that as a damped harmonic oscillator — bounce maps to the damping ratio, duration to the settle time — and then samples the curve into a CSS
linear() easing so a plain transition can play it. Close enough to judge feel, and to argue about whether 0.2 belongs in a crisp library.The sampled curve is also the escape hatch: a component that cannot take a JS dependency can still run the spring shape from CSS. What it cannot do is interruption. Press the button below twice in quick succession — the box retargets from wherever it is, but from a standstill, because a curve has no velocity to carry. A real spring integrator does. So the fallback is for shape, and gesture work wants the JS spring.Sampled at 16 points here so it is readable; the mirror emits 64. Overshoot 0.2%, which is the part a cubic-bezier cannot express without going out of its 0–1 box.press — CSS fallback, sampled from the spring — css/* No JS dependency. Shape only: an interruption restarts from rest. */.thing {transition-property: transform;transition-duration: 300ms;transition-timing-function: linear(0.0000,0.0422,0.1396,0.2606,0.3856,0.5032,0.6077,0.6966,0.7700,0.8290,0.8754,0.9110,0.9380,0.9579,0.9723,0.9826,0.9897);}
Open question: should springs be theme-tunable?
A theme already retunes the whole duration scale from three numbers — a snappy theme sets{fast: 100, medium: 250, ratio: 0.75} and every primitive moves with it. Springs have no such axis, so today they are the same everywhere no matter what the theme says. If one theme is meant to feel bouncier than another, springs belong in the theme contract alongside the duration scale — and adding them after the contract ships is a breaking change to it. This wants deciding before the token work lands, not after.Springs are defined natively in the mirror rather than parsed out of CSS, since they have no CSS form to parse. See JS token mirror for the emitted shape and Export tuning for what ships.packages/core/src/theme/expandMotionScale.ts — the axis today, and the question — tsexport interface MotionScaleConfig {fast: number; // 175 — micro-interactionsmedium: number; // 410 — entrance and exitslow?: number; // 975 — continuousratio: number; // 0.75 — min = base × ratio, max = base ÷ ratioeasing?: string; // overrides --ease-standard// Proposed, and the decision this page is asking for. Optional is not the// same as cheap: once themes ship without it, a theme author who tunes// duration and finds springs unmoved has been told the contract covers// motion when it covers only half of it.spring?: Record<'press' | 'swap' | 'panel' | 'layout', {duration: number; bounce: number}>;}