Motion

Motion confirms actions and creates continuity. Nothing else. No gratuitous animation. No entrance animations. No scroll-triggered reveals. Every transition exists because removing it would make the interface feel broken or ambiguous.

Signature easing

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

A decelerating settle curve. The element moves quickly at first and eases into its final position, like a physical object coming to rest. Used for spatial transitions where the shape of the curve is perceptible: underline sweeps, weight shifts, layout expansion, view transitions. Hover over any link on this page — the underline sweep uses this curve.

Secondary easing

ease

The CSS default. Used for simple property changes where the curve shape won’t be noticed: color shifts, opacity fades, border-color changes. There’s no reason to use the signature curve when the human eye can’t distinguish it from ease on a 0.2s color fade.

Animation inventory

Grouped by what triggers the motion.

Hover transitions

ElementPropertiesDurationEasing
Prose link underline sweepbackground-size0.3ssignature
Prose link colorcolor0.2sease
Nav nameplate weight shiftfont-variation-settings0.4ssignature
Post title weight + colorfont-variation-settings 0.4s signature, color 0.3s ease
Side rail link colorcolor0.2sease
Theme toggle colorcolor0.2sease
Accent dot borderborder-color0.15sease
Accent option scale + bordertransform, border-color0.15sease
Font size controlfont-size0.15sease

State changes

ElementPropertiesDurationEasing
Toast show/hidetransform 0.3s signature, opacity 0.3s ease
Accent picker expandmax-width 0.3s signature, opacity 0.2s ease, margin-left 0.3s signature
Share modal overlayopacity, visibility0.2sease
Share modal buttonsborder-color, background0.25sease
Side rail visibilityopacity0.4sease
Body background (theme switch)background0.3sease
Details toggle openopacity, transform0.25sease

Decorative

These are the only animations gated behind prefers-reduced-motion: no-preference:

ElementAnimationDurationEasingIteration
Live asterisk breatheopacity 1 to 0.154sease-in-outinfinite
Accent hint pulsescale 1 to 1.30.6sease2

Both are non-interactive. They communicate liveness (the asterisk) and invite discovery (the hint pulse). Neither conveys essential information.

View transitions

Cross-document view transitions are enabled globally:

@view-transition {
  navigation: auto;
}

Two transition groups are defined:

GroupDurationEasingPurpose
root150mseaseContent crossfade between pages
nameplate200mseaseNameplate morphing (size/position shift between homepage and interior)

The nameplate transition creates continuity as you move between pages. The content crossfade is fast enough to feel instant while preventing a hard cut.

Scroll progress

The scroll progress bar uses animation-timeline: scroll() (behind @supports) with a linear timing function. This is scroll-linked, not time-based, so easing curves don’t apply.

Reduced motion

Two levels of reduction.

Decorative animations (breathe, hint pulse, details toggle) are wrapped in @media (prefers-reduced-motion: no-preference) and only run when the user hasn’t requested reduced motion.

Blanket override catches everything else:

@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    transition-duration: 0.01ms !important;
    animation-duration: 0.01ms !important;
    animation-iteration-count: 1 !important;
  }
}

This collapses all transitions to effectively instant. Hover state changes still occur (confirming user actions), they just skip the interpolation. View transitions get their own override, zeroing out animation-duration on all ::view-transition-* pseudo-elements.

The 0.01ms value rather than 0s avoids edge cases where some browsers skip transitionend events entirely when the duration is zero.

Do’s and don’ts

DoDon’t
Use signature easing for spatial transitionsUse linear or ease-in for UI transitions
Use ease for simple color/opacity changesUse signature easing where the curve won’t be perceived
Gate non-interactive animation behind prefers-reduced-motionAssume hover transitions need motion gating
Keep durations between 0.15s and 0.4sAdd animations longer than 0.5s

Specimens

cubic-bezier(0.22, 1, 0.36, 1)
Decelerating settle — fast start, gentle landing. Click to play.
Weight shift font-variation-settings 400 → 500
Underline sweep → background-size 0% → 100%
Color transition --text-2 → --accent
* The live indicator — accent-colored, 4s ease-in-out infinite

All decorative animations respect prefers-reduced-motion. Hover transitions do not need gating — they are responses to user action, not ambient decoration.