Motion
Caret motion is bounded, intentional, and gated. Every animation has a designed start and a designed end — spinners resolve into checkmarks, selections slide, progress pulses, errors reveal. The rules below apply to every component that animates.
Hard rules
- Duration is bounded — no animation runs longer than 300ms unless it's representing actual ongoing work (spinner while a deploy runs).
- Inline-safe — animations don't push surrounding characters around between frames. If a frame rotates, it's locked to a single character cell.
- Reduced motion respected — all animation is disabled when
prefers-reduced-motionis on, whenCARET_REDUCED_MOTION=1, or when stdout is not a TTY. - Token-based — durations and frame rates come from the theme, not magic numbers in components.
Duration tokens
| Token | Default (ms) | Used by |
|---|---|---|
| duration.instant | 60 | Cursor-blink window, sub-frame timing |
| duration.fast | 120 | Color/border transitions |
| duration.default | 200 | Spinner morph, prompt resolve, selection slide |
| duration.slow | 300 | Reveal, modal open, boot step |
Frame rates
Stepped animations (spinner, typewriter) tick on a fixed interval. Frame rates are also tokens, so an entire CLI's animation feel can be tuned in one spot.
| Token | Default (ms) | Used by |
|---|---|---|
| spinnerFrameMs | 80 | Braille spinner step interval |
| blinkMs | 1050 | Block cursor blink cycle |
| typewriterMs | 24 | Character-by-character reveal |
Easing
Caret exports an easing namespace with the standard curves used internally. Custom components are encouraged to consume them rather than inline cubic-bezier strings.
import { easing } from '../caret/lib/motion.js'
easing.linear // (t) => t
easing.easeOut // (t) => 1 - Math.pow(1 - t, 3)
easing.easeInOut // smoothstepframeLoop helper
For custom timed animation, use frameLoop. It wraps setInterval with reduced-motion gating and a clean cancel signature.
import { frameLoop } from '../caret/lib/motion.js'
const cancel = frameLoop(80, (frame) => {
// 80ms ticks. Returns false to stop, anything else to continue.
})
// Later — bail out
cancel()Reduced motion behavior
When reduced motion is active, components skip the animation but keep the semantic transition. Examples:
| Component | Normal | Reduced motion |
|---|---|---|
| spinner | Braille rotation, then morph to ✓ | Static · then ✓ |
| typewriter | Character-by-character reveal | Whole text appears at once |
| reveal | Each line fades in sequentially | All lines appear at once |
| modal | Fade in over 300ms | Appears immediately |
| progress | Percent animates from current to new | Percent jumps directly |
Capability detection — including reduced-motion sources — is documented at Capability detection.