GodUIGodUI
129Follow on X

Anatomy of Stepper

How Stepper combines three motion techniques (a CSS color transition, a spring-filled connector, and an SVG pathLength draw) into one step-forward beat.

01/03One stateFor(i), three tones
•
•
Complete
border-fg, filled, check drawn
Active
border-fg, hollow, ring-4
Upcoming
border-border, muted text
tsx
const stateFor = (i: number): StepState =>i < active ? "complete" : i === active ? "active" : "upcoming";
01

One stateFor(i), three tones

Stepper looks like one animation when a step completes, but it's three independent techniques layered together, each picked for what it animates.

Every circle's tone comes from a single comparison against active, with no per-step config or variants map. Connectors follow the same logic: the one between step i and i + 1 fills only once active > i, meaning the step before it is done and the user has moved past it.

02/03The motion

complete

•

active

Circle
CSS transition, 250ms ease
Connector
spring(320, 32, 0.9), slight overshoot
Check
pathLength 0 → 1, 0.3s easeOut
tsx
// the circle's border/background/text color — a plain CSS transitionclassName="[transition:background-color_250ms_ease,border-color_250ms_ease,color_250ms_ease,box-shadow_250ms_ease]"// the connector fill — a spring, so it can overshoot slightly<motion.spananimate={{ scaleX: filled ? 1 : 0 }}transition={{ type: "spring", stiffness: 320, damping: 32, mass: 0.9 }}/>// the checkmark — a pathLength tween, drawing the stroke rather than fading it in<motion.pathd="M5 13l4 4L19 7"initial={{ pathLength: 0 }}animate={{ pathLength: 1 }}transition={{ duration: 0.3, ease: "easeOut" }}/>
02

The motion

Advancing a step fires three different animations at once, and each one would be the wrong technique for the other two jobs.

A color swap has no meaningful motion to it, so a transition is cheaper than mounting a motion component for a property that's either on or off. The connector is motion (a line growing across visible space), so it gets the spring that gives every other GodUI surface transition its weight. The checkmark uses neither a fade nor a scale: pathLength draws the stroke the way a pen would, which a spring or an opacity fade can't reproduce. Each animation type matches the shape of its change.

Horizontal vs. vertical: the same Connector component renders either orientation from one prop. Only the animated axis and its origin flip.

tsx
 
animate={horizontal ? { scaleX: filled ? 1 : 0 } : { scaleY: filled ? 1 : 0 }}
className={horizontal ? "mt-[17px] h-0.5 flex-1" : "my-1 w-0.5 flex-1 self-center"}

scaleX from origin-left reads as "filling left to right"; scaleY from origin-top reads as "filling downward". It is the same spring, aimed along whichever axis the layout flows.

03/03Result
Account
Email & password
2
Profile
Name & avatar
3
Workspace
Invite your team
4
Done
All set
03

The result

One state comparison drives three animations, each matched to the change it shows.

Accessibility

The active step carries aria-current="step" so assistive tech announces where the user is without relying on visual state alone. Under prefers-reduced-motion, every transition collapses to { duration: 0 } and the checkmark's initial is skipped (reduceMotion ? false : { pathLength: 0 }), so steps switch instantly with the check already fully drawn.

Motion Score

StepperCC: Paint-triggering
Cbox-shadowstep-state shadow shares its transition with background/border/color paint
SscaleScale spring / press
SpathLengthSVG pathLength reveal
Each property is graded by how the browser runs it, from S (composited off the main thread) down to F (layout thrashing); the component takes the worst. MotionScore methodology →