Skip to content
VVVER

Design System / 05

Motion

Four curves with distinct jobs, brief responses to input, and slower presentation motion that never carries meaning by itself.

Principles

Motion follows intent: each curve has a job, timing scales with the surface, and an exit always finishes faster than the entrance it reverses. Slow presentation never delays access to content or input.

Interaction dims, never highlights.

Attention is spent by stepping presence down a ladder — 60 / 30 / 15% — rather than by brightening anything up. The documented recipes interpolate only transform and opacity. Color, contrast, crop, and filters remain static. Each recipe has a prefers-reduced-motion path that presents a stable, complete state.

Technical detail

Curves
Entries use --ease-out-quint, masked display --ease-out-expo, curtains and drawn rules --ease-in-out-quart, and exits --ease-in-quad.
Timing
Direct interaction at or below 400ms; large surfaces 400–600ms; ambient media 700–1000ms; masked display or rule drawing up to 1200ms.

Transform and opacity — correct

transform: translateX()
Neighbour stays put

Hover: the bar moves over its neighbour. Composited, so nothing else on the row is disturbed.

Layout properties — avoid

width
Neighbour gets pushed

Hover: animating width reflows the row on every frame and shoves the neighbouring text sideways.

Easing

Ease out quint

--ease-out-quint

cubic-bezier(0.22, 1, 0.36, 1)

Entrances and feedback: button lifts, reveals, crossfades. Fast start, long settle.

Ease in-out quart

--ease-in-out-quart

cubic-bezier(0.76, 0, 0.24, 1)

Curtains and self-drawing rules. Symmetrical acceleration and deceleration.

Ease out expo (display)

--ease-out-expo

cubic-bezier(0.16, 1, 0.3, 1)

Display reveals only: masked words, letter rises. Near-instant start, very long settle — never on interactive controls.

Ease in quad (exit)

--ease-in-quad

cubic-bezier(0.55, 0.085, 0.68, 0.53)

Exits only: fade-outs, underline wipe-off, dismissals. Accelerates away.

Timing

Duration scale

150ms

200ms

250ms

300ms

400ms

550ms

700ms

1000ms

1200ms

150msTiny returns and control exits

200msOpacity feedback and popup exits

250msLabel returns and underline wipes

300msPopup and micro-reveal entries

400msUpper edge for direct interaction

550msDrawer and large-panel entrances

700msText reveals and curtain exits

1000msAmbient media crossfades

1200msMasked display and rule draws

Micro-interactions

Live specimens — hover, focus, and press them.

Link slide

View choreography

A clipped twin enters over 300ms on the entry curve; the label returns over 250ms on the faster exit curve.

One-pixel response

Hover retreats 1px and dims over 200ms; release returns over 150ms. Press moves 1px below rest without changing color.

Underlined link

Review accessibility

Directional wipe: the hairline exits right on the exit curve while its replacement enters from the left on the entry curve.

Media de-emphasis

A woman seated on the steps of a garden building

The photograph steps down to the first dim rung and tightens its crop by 1.5%. The linked content and focus outline stay present; reduced motion keeps the crop still.

Component reference

Choreography

Live specimen

Rendered output

Mask reveal

Function Before Effect

Words rise out of clipped wrappers over 1200ms on the display curve. The fast initial move and long settle make the reveal legible without turning it into control feedback.

Rule draw

Hairlines draw from the left in 1200ms on ease-in-out-quart; three short lead-ins create a drafting sequence.

Decode

Plate 07 / 2026

Graphemes lock in left to right over 900ms. Unsettled positions sample only numerals and hairline glyphs; use it for labels and counters, never body text.

Stagger

Source

Date

Plate

Approval

A 120ms lead-in, then 50ms between rows—the same masked cadence as the mobile ink curtain.

Micro reveal

Entering content

Enters over 300ms from −6px on the entry curve; exits over 200ms to −4px on the exit curve, then unmounts.

MaskReveal

Each word rises out of its own clipped wrapper when the element scrolls into view—1200ms on the mask-tier expo curve. Requested word delays retain their cadence and are clamped only for unusually long headings. Reduced motion lands on the final state.

API reference

Inputs, types, and behavior

Inputtext
Typestring
BehaviorWhitespace-preserving word wrappers; graphemes remain intact and an sr-only copy carries the accessible text.
Inputas
Type"h1" | "h2" | "h3" | "p" | "span"
BehaviorRendered element, default "span".
Inputstagger
Typenumber = 30
BehaviorRequested per-word delay; ordinary headings preserve it, with a 600ms upper staging window for extreme strings.
Inputdelay
Typenumber = 0
BehaviorRequested lead-in, sanitized inside the same 600ms staging window.
InputclassName
Typestring?
BehaviorMerged onto the rendered element.

DecodeText

Graphemes lock in left to right over 900ms, scrambling through a charset of numerals and hairline glyphs until each settles. One-shot on scroll-in; reduced motion renders the final text instantly.

API reference

Inputs, types, and behavior

Inputtext
Typestring
BehaviorThe final string; the scramble rig is aria-hidden behind an sr-only copy.
Inputduration
Typenumber = 900
BehaviorTotal lock-in time; non-finite input uses 900 and zero resolves instantly.
InputclassName
Typestring?
BehaviorMerged onto the root element.

RuleDraw

A decorative hairline scales from its left edge over 1200ms on the curtain curve. It is hidden from assistive technology; reduced motion and no script show the complete rule immediately.

API reference

Inputs, types, and behavior

Inputdelay
Typenumber = 0
BehaviorLead-in in ms, sanitized to 0–600; non-finite input uses zero.
InputclassName
Typestring?
BehaviorMerged last onto the hairline element.

Component reference

Scroll settle

A hero-only entrance: media settles into rest as it enters, complete before it reaches the reader’s eyeline.

Live specimen

Rendered output

Technical detail

Settle
Media moves from 92% scale / 80% opacity to rest, mapped through a fast-start quintic polynomial.
Scrub band
Runs from viewport entry to mid-viewport, read in the capture phase so nested overflow containers drive it too.
Reduced motion
Skipped entirely.

Scroll settles

API reference

Inputs, types, and behavior

Inputchildren
TypeReactNode
BehaviorThe media block that settles — a single hero image or figure.
InputclassName
Typestring?
BehaviorMerged onto the transform-gpu wrapper.

Component reference

Scroll reveal

The same reveal pattern generalized to any children: the group flips in-view once and each child arrives one by one on an indexed delay.

Live specimen

Rendered output

Technical detail

Hook
useInViewOnce flips the whole group in-view once; each child gets an indexed transition delay.
Variants
kind="text" lifts 8px and fades; kind="media" fades only, so images never slide.
Timing
Text enters over 700ms and media over 1000ms; a 480ms staging window keeps long groups finite without erasing their cadence.
Reduced motion
Reduced motion and no-script show children immediately.

Text register

Features

A complete reveal specimen

The group arrives in source order and remains readable before client-side JavaScript runs.

Media register

A woman seated on the steps of a garden building

API reference

Inputs, types, and behavior

Inputchildren
TypeReactNode
BehaviorEach child is wrapped in a block with an indexed transition-delay.
Inputkind
Type"text" | "media"
BehaviorDefault "text" lifts and fades over 700ms; "media" fades in place over 1000ms.
Inputstagger
Typenumber = 80
BehaviorRequested per-child stagger, clamped by item count inside a 480ms staging window.
Inputas
Type"div" | "section"
BehaviorContainer element, default "div". Keep it a flow container — children are wrapped in <div>s.
InputclassName
Typestring?
BehaviorOn the container — put grid/spacing here; the per-child wrappers stay transparent.

Component reference

Motion infrastructure

Two exported hooks power the package’s scroll-triggered reveals and position-mapped effects. Both use rAF-coalesced position checks, so anchor jumps and End-key jumps cannot skip a threshold crossing.

Live specimen

Rendered output

Scrub

Progress 0%

The hairline scales with this box’s travel through the viewport — scroll to scrub it back and forth.

One-shot reveal

Observed section

Reveals once when it first enters the viewport, then stays put.

useScrollProgress(options?)

Returns { progress, ref } — a 0–1 scrub value for the element as it travels through the viewport, measured per scroll frame. It is position-derived, so it stays live under reduced motion. Consumers must disable any animation driven by the value.

API reference

Inputs, types, and behavior

Inputstart
Typenumber = 1
BehaviorViewport-height fraction where the element's top starts the mapping (1 = bottom edge).
Inputend
Typenumber = 0
BehaviorViewport-height fraction where the element's bottom ends the mapping (0 = top edge).

useInViewOnce(factor?)

Returns { inView, ref } — flips to true the first time the element's top crosses factor × viewport height (default 0.92) and never flips back. Under reduced motion it short-circuits to visible.

API reference

Inputs, types, and behavior

Inputfactor
Typenumber = 0.92
BehaviorTrigger line as a fraction of viewport height.

Accessibility

Nothing that moves is load-bearing for understanding this chapter. Every recipe has a static path that presents the complete result.

Reduced motion

prefers-reduced-motion: reduce removes transitions and keyframes, completes one-shot JavaScript reveals, and leaves all content present.

Focus

Focus stays visible even where pointer interaction dims the surrounding content.

Position hooks

useScrollProgress still reports position under reduced motion, because a number is not movement.

Consumer duty

Any effect driven by a reported position must be kept static when reduced motion is requested.