Gesso

Motion

Motion is information, not decoration. Springs in three tiers, one hover fill, and values that change in place.

Spring Tiers

Import spring from components/fluid/lib/springs and pick a tier; exits use spring.tier.exit. Press Play again mid-move: a spring answers the interruption instead of finishing its old path.

spring.fast
Fills, fades, swapped values
spring.moderate
Slides, resizes, highlights
spring.slow
Dialogs and sheets

Fluid Hover

Menus, selects, command lists, and nav share one fill that glides to the item nearest the pointer and follows the keyboard. Wrap them in FluidHoverScope; items paint no hover of their own. List rows, table rows, and settings option rows are items in their own right and take a plain hover instead. Every moving highlight, including hover fills, selected rows, segmented controls, and tab indicators, moves on HIGHLIGHT_TRANSITION, a 150ms ease-out, so they all feel the same.

Values Change in Place

Any single line of text that changes, like a status, a hint, a label, or a count, rolls to its new value with TextSwap: the old line rises out as the new one rises in from 3px below, as the settings save footer does. Ticker does the same for numbers. It changes where it stands, so a live list never jumps. Fade is for swapping whole blocks and icons.

Guidance

Motion is information, not decoration: it shows where something came from, where it went, or what changed. Interface motion follows Fluid Functionalism and uses springs, because a spring answers an interruption naturally when someone reverses mid-move. Pick one of three tiers and never hand-write a duration: fast (about 80ms, no bounce) for fills, fades, and swapped values; moderate (about 160ms, critically damped) for anything that slides or resizes, such as menus and height reveals; and slow (about 240ms, a little bounce) for the largest surfaces, such as dialogs and sheets. Exits use the tier's quicker tween with no bounce, so a dismissal reads as final. Highlights that move between items are the one exception: hover fills, selected rows, segmented controls, and tab indicators all use HIGHLIGHT_TRANSITION (150ms ease-out) from motion-primitives, so every one of them glides the same way. Menus, selects, command lists, and nav share one hover fill that glides to the item nearest the pointer and follows the keyboard. Rows that stand on their own take a plain hover instead: list rows and settings option rows light in place, and table rows light only when the table is interactive (its rows open something); a read-only table doesn't react to the pointer. Popups (menus, popovers, selects) share one surface and entrance from components/ui/popup.ts. A single line of text that changes value (a status, hint, label, or count) rolls in place with TextSwap: out upward, in from 3px below, on the moderate spring. Long tweens are reserved for data, such as a meter filling or a number counting up. Avoid looping or attention-grabbing animation, and honor prefers-reduced-motion: keep fades, drop travel.