Manicat UI

Motion

Motion presets shared by every Manicat UI component, from instant presses to morphing shapes.

Every animation in Manicat UI comes from one file, motion-tokens.ts. Components import the presets rather than writing their own durations, which is what keeps a page of them feeling like one product.

Durations

PresetSecondsUsed for
instant0.12Presses, colour changes, anything the finger is waiting on
fast0.16Icons and labels crossfading
exit0.18Leaving labels, dismissed panels
standard0.24The default for most transitions
considered0.48Deliberate reveals, larger surfaces

Springs

Springs carry anything that moves while it is already on screen.

PresetCharacterUsed for
snappyvisualDuration 0.26, bounce 0.12Presses, toggles, thumbs, small indicators
smoothvisualDuration 0.4, bounce 0Panels, height changes, layout shifts
morphvisualDuration 0.42, bounce 0.16Shape morphs, shared highlights, width changes
responsivestiffness 520, damping 38Drag and gesture tracking
gentlestiffness 340, damping 34Larger surfaces that should not feel quick

Scale rides the spring, opacity and blur tween. Blur never overshoots below zero, which is why crossfades read as depth rather than as a smear.

Presses, toggles and anything the finger is waiting on.

Reduced motion

Every component reads useReducedMotion() and swaps movement for a short fade. Nothing is disabled outright, so state changes stay legible, and no animation depends on a transform to communicate.

import { useReducedMotion } from "motion/react";

const reduceMotion = useReducedMotion() ?? false;

<motion.span
  initial={reduceMotion ? { opacity: 0 } : { opacity: 0, y: 4 }}
  animate={{ opacity: 1, y: 0 }}
  transition={reduceMotion ? { duration: motionTokens.duration.instant } : motionTokens.spring.snappy}
/>;

The CSS side follows the same rule: transitions are shortened rather than removed, so hover and focus still answer.

On this page