Motion
Spring recipes, timing curves, travel, and accessible overrides.
Choose a preset for a coherent starting point. Use motionOptions to tune individual transitions without replacing the component's interaction behavior. Motion remains a package dependency; the library owns the typed configuration and how it applies to the menu.
Explore theme and motion together in the Design Playground.
Presets
| Preset | Popup | Pointer highlight | Label / chevron |
|---|---|---|---|
none | Immediate | Immediate | Immediate |
subtle | 120 ms tween, scale 0.98, travel 4 px | 90 ms | 120 / 140 ms |
expressive | Spring: stiffness 420, damping 26, mass 1; scale 0.94, travel 4 px | 180 ms | 120 / 140 ms |
motion="none" disables animation even when overrides are present. Preset changes keep explicit overrides; remove motionOptions to return to the new preset's defaults.
Springs
Shared spring recipes are exported as motionTokens.spring. Each is a valid motionOptions.transition value.
| Recipe | Stiffness | Damping | Mass | Use |
|---|---|---|---|---|
snappy | 520 | 38 | 1 | Small, responsive changes |
smooth | 340 | 38 | 1 | Restrained panel motion |
morph | 420 | 26 | 1 | A more elastic change; the expressive default |
gentle | 220 | 30 | 1 | Slower, calmer movement |
import { DropdownMenu, motionTokens } from '@uthereal/ui/dropdown-menu'
;<DropdownMenu
items={items}
motionOptions={{ transition: motionTokens.spring.smooth }}
/>For your own spring, provide type: 'spring' and any of stiffness, damping, or mass. Unspecified spring fields use morph defaults. A spring is physics-based; it does not accept a tween duration or easing curve.
Easing and duration
All public duration options use milliseconds. Motion itself expects seconds for tweens; the component converts at the rendering boundary.
motionTokens.duration provides fast: 120, standard: 240, and considered: 480. motionTokens.easing provides standard: 'easeInOut', enter: 'easeOut', and exit: 'easeIn'. You can also choose linear.
<DropdownMenu
items={items}
motionOptions={{
transition: {
type: 'tween',
duration: motionTokens.duration.standard,
ease: motionTokens.easing.enter,
},
enter: { opacity: 0, scale: 0.97, offset: 8 },
exit: { opacity: 0, scale: 0.99, offset: 3 },
highlightDuration: 100,
labelDuration: 120,
chevronDuration: 180,
}}
/>The popup settles at opacity 1, scale 1, and zero travel. Enter/exit offsets are positive distances toward the trigger, on the axis of the actual placement. Submenus and collision-flipped menus therefore travel in the appropriate direction. transition controls both entrance and exit; there is no looping or delay option.
Option reference
Generated from the schema. Defaults below use subtle; spring-only fields show the fallback spring values. Numeric geometry uses pixels, rotation uses degrees, opacity and scale are unitless. A zero duration or offset is preserved.
| Option | Default | Values / purpose |
|---|---|---|
transition.type | tween | tween / spring Popup transition kind |
transition.duration | 120 | 0–2000 Milliseconds; zero is immediate |
transition.ease | easeOut | linear / easeIn / easeOut / easeInOut Timing curve |
transition.stiffness | 420 | 1–1000 Spring stiffness |
transition.damping | 26 | 1–100 Resistance to oscillation |
transition.mass | 1 | 0.1–10 Simulated mass |
enter.opacity | 0 | 0–1 Starting or ending opacity |
enter.scale | 0.98 | 0.5–1.2 Starting or ending scale |
enter.offset | 4 | 0–40 Travel toward the trigger, px; follows actual placement |
exit.opacity | 0 | 0–1 Starting or ending opacity |
exit.scale | 0.98 | 0.5–1.2 Starting or ending scale |
exit.offset | 2 | 0–40 Travel toward the trigger, px; follows actual placement |
highlightDuration | 90 | 0–2000 Pointer highlight travel, ms; keyboard is always immediate |
labelDuration | 120 | 0–2000 Trigger text transition, ms |
labelOffset | 3 | 0–24 Trigger text travel, px |
chevronDuration | 140 | 0–2000 Chevron rotation, ms |
chevronRotation | 180 | -360–360 Open chevron angle, degrees |
chevronEase | easeInOut | linear / easeIn / easeOut / easeInOut Chevron timing curve |
Reduced motion and input
The component watches the device's prefers-reduced-motion setting, including changes while mounted. Reduced motion removes popup travel, label transitions, pointer-highlight movement, and chevron animation, regardless of the preset or supplied overrides. Focus, state changes, and action feedback remain available.
Keyboard navigation always updates its highlight immediately. Timing overrides apply to pointer movement; repeated arrow presses never wait for an animation. Avoid using motion alone to communicate a result.
Interruptions and live edits
Motion handles interrupted transitions. Closing makes the exiting popup inert immediately; an animation never delays an action or leaves a closing menu interactive. Changing design or motion arguments preserves a mounted menu's selections and IDs. Changing entrance settings is easiest to compare by reopening the menu.
Use a DropdownPreset to share motion and motionOptions across an application. Nested fields merge; changing transition.type replaces its old variant, so tween settings cannot leak into a spring. Agents use the same options and validation rules; see Building with agents.