Speed
Slow motion multiplier
1×
Reduced motion
Show

Springs

JS mirror only — no CSS form
A spring is described by duration and bounce, not by a curve, and it has no CSS representation at all — which is why the proposal keeps springs in the JS mirror rather than the token file. What they buy is interruption: a spring carries velocity through a change of target, where a keyframe restarts from zero. That is the whole case for having them, and it only pays off in gesture-driven and interruptible motion.Decide here: Whether the four named springs feel right, and whether they belong in the theme contract.
Bounce belongs in 0.1–0.3 for a crisp library
Rubric criterion 11 is that visible bounce is for drag-to-dismiss and playful moments; a dashboard stays crisp. Every slider below runs to 0.6 on purpose. At bounce 0.3 the box passes its target by 4.6% and settles back; at 0.6 it passes by 25.4% and reads as a toy. Overshoot depends only on bounce, not on duration, so this is the one number worth arguing about — and it is easier to agree on by looking than by reading it.

press

300ms · bounce 0.10
Pressable surfaces
spring — press
CSS counterpart — --duration-press + --ease-entry
Duration
0.30s
Bounce
0.10

swap

400ms · bounce 0.15
Content trading places inside a control
spring — swap
CSS counterpart — --duration-enter + --ease-move
Duration
0.40s
Bounce
0.15

panel

500ms · bounce 0.20
Overlay entrances, sheet travel
spring — panel
CSS counterpart — --duration-overlay + --ease-drawer
Duration
0.50s
Bounce
0.20

layout

500ms · bounce 0.15
Sliding indicators, morphing surfaces, reorder
spring — layout
CSS counterpart — --duration-overlay + --ease-move
Duration
0.50s
Bounce
0.15

What the preview actually is

honest about the model
An approximation, and it is worth knowing where it stops being one.
Motion 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.
press — CSS fallback, sampled from the springcss
/* 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);
}
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.

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.
packages/core/src/theme/expandMotionScale.ts — the axis today, and the questionts
export interface MotionScaleConfig {
fast: number; // 175 — micro-interactions
medium: number; // 410 — entrance and exit
slow?: number; // 975 — continuous
ratio: number; // 0.75 — min = base × ratio, max = base ÷ ratio
easing?: 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}>;
}
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.