Motion
Motion
Motion is feedback, not decoration. Hulian UI defines easing and duration tokens so CSS utilities and motion-driven animation share the same feel.
First ask whether it should move
Decide by how often users see the action, not by how attractive the animation looks. Motion on frequent actions makes software feel slow.
| Frequency | Typical scenario | Guidance |
|---|---|---|
| 100+ times a day | Command palette and keyboard shortcuts | No animation |
| Dozens of times a day | Hover states and list navigation | Remove or greatly reduce |
| Occasionally | Dialogs, drawers, and toasts | Standard motion |
| Rarely or once | Onboarding and success celebrations | A moment of delight is appropriate |
Easing curves
Hover a track to compare its feel. Hulian UI replaces the weak default ease-out with a more decisive shared curve.
Deceleration (default)
ease-outcubic-bezier(0.16, 1, 0.3, 1)Entrance, exit, hover, and press feedback. It starts at full speed, so users see an immediate response. Use this curve when unsure.
Accelerate, then decelerate
ease-in-outcubic-bezier(0.65, 0, 0.35, 1)Movement or transformation within the viewport. Both ends are gentle, like a physical object starting and stopping.
Drawer (iOS and Ionic)
--ease-drawercubic-bezier(0.32, 0.72, 0, 1)Full-screen sliding surfaces such as Drawer and ActionSheet. Its longer tail prevents large surfaces from stopping abruptly.
Duration
Keep interactive motion within 300ms. A 180ms menu feels much faster than a 400ms one.
| Token | Duration | Utility | Used for |
|---|---|---|---|
| fast | 150ms | duration-150 | Press feedback and close micro-interactions (100-160ms) |
| base | 200ms | duration-200 | Overlay entrances and exits: Tooltip, Popover, Select, and Menu |
| slow | 300ms | duration-300 | Large transitions: Drawer and ActionSheet panels |
| entrance | 600ms | duration-600 | First-paint reveals and scroll entrances, not interaction feedback |
Press feedback
Every pressable element should respond immediately so the interface acknowledges the action.
Scale ordinary controls to 0.97 on press and large cards to 0.99 so their children do not appear to jump.
// Component already using motion import { pressable } from "@hulianui/ui"; <m.button whileTap={pressable.whileTap} transition={pressable.transition} /> // Pure CSS alternative without the motion runtime import { pressableClass } from "@hulianui/ui"; <button className={cn("…", pressableClass)} />
Overlays grow from their trigger
An anchored overlay should open from its trigger, not from its own center.
/* Base UI exposes the computed origin on the Positioner */
<Popup className="origin-[var(--transform-origin)] data-[starting-style]:scale-95 …" />Accessibility
Reduced motion does not mean removing all feedback.
This media query is the library's job
Responding to prefers-reduced-motion belongs to the component, not to the consumer. Nobody should have to write !important or guess at the library's internal DOM structure to switch an animation off - a selector like that fails silently when the structure changes, and what fails is an accessibility preference, with no error to show for it. If a component still moves under reduced motion, file it as a bug.
usePrefersReducedMotion() For hand-rolled motion (canvas, rAF, your own inline style transitions), read the same signal from usePrefersReducedMotion() instead of writing another matchMedia. It is false during SSR and in the first HTML frame, then corrects itself right after hydration.