Motion System
veloria-ui/motion is a zero-dependency animation layer built on the Web Animations API. No Framer Motion, no GSAP. 16 presets. prefers-reduced-motion aware.
Quick start
import {Animated,MotionModal,MotionCard,MotionDrawer,} from "veloria-ui/motion";// Pre-wrapped component — drop-in with motion prop<MotionModal motion="fade-scale" open={isOpen} onOpenChange={setOpen} title="Hello" /><MotionDrawer motion="slide-left" open={isOpen} onOpenChange={setOpen} side="left" />// Conditional element with animated enter/exit<Animated show={isVisible} motion="fade-up"><p>Animates in and out</p></Animated>// Mount-only animation<MotionCard motion={{ preset: "fade-up", delay: 150 }}>Content</MotionCard>
Presets
Pass any preset name as a string to motion=.
| Preset | Description |
|---|---|
| "fade" | Simple opacity fade |
| "fade-up" | Fade in from below |
| "fade-down" | Fade in from above |
| "fade-left" | Fade in from the right |
| "fade-right" | Fade in from the left |
| "fade-scale" | Fade + scale from 95% → 100% |
| "slide-up" | Translate from 100% below, no fade |
| "slide-down" | Translate from 100% above |
| "slide-left" | Translate from 100% right |
| "slide-right" | Translate from 100% left |
| "zoom" | Scale from 50% with fade |
| "zoom-out" | Scale from 110% with fade |
| "flip" | 3D Y-axis perspective flip |
| "flip-x" | 3D X-axis perspective flip |
| "bounce" | Elastic entrance with spring easing |
| "none" | No animation (useful for accessible overrides) |
<Animated>
Wraps any HTML element with animated enter/exit. The element is removed from the DOM after the exit animation completes (unless keepMounted is set).
import { Animated } from "veloria-ui/motion";<Animatedshow={isVisible}motion="fade-up"as="section"keepMounted={false}className="my-class">{children}</Animated>
| Prop | Type | Default | Description |
|---|---|---|---|
| show | boolean | true | Controls visibility and triggers enter/exit |
| motion | MotionPreset | MotionConfig | — | Animation configuration — preset string or config object |
| as | React.ElementType | "div" | HTML element to render |
| keepMounted | boolean | false | Stay in DOM after exit (hidden), instead of being removed |
<MotionPresence>
Manages enter/exit for children that mount and unmount. When the child is removed from the React tree,MotionPresence plays its exit animation before removing it from the DOM — identical to Framer Motion's AnimatePresence.
import { MotionPresence } from "veloria-ui/motion";<MotionPresencemotion="zoom"onExitComplete={() => console.log("fully gone")}>{isOpen && <Panel key="panel" />}</MotionPresence>
The child must have a stable key prop and must accept a ref.
withMotion()
Higher-order component that wraps any component and adds a motion prop. Zero overhead — if you don't pass motion, there is no wrapper or animation cost.
import { withMotion } from "veloria-ui/motion";import { Modal } from "@/components/ui/modal"; // your local copyconst MotionModal = withMotion(Modal, {visibleProp: "open", // watch this prop for enter/exit});<MotionModal motion="fade-scale" open={isOpen} onOpenChange={setOpen} title="Hello" />// For mount-only animation (no open prop), pass { visibleProp: null }const MotionCard = withMotion(Card, { visibleProp: null });<MotionCard motion={{ preset: "fade-up", delay: 200 }}>Animates on mount only</MotionCard>
useMotion()
Low-level hook. Attach animated enter/exit to any DOM element ref you control.
import { useMotion } from "veloria-ui/motion";function MyPanel({ show }: { show: boolean }) {const { ref, isVisible } = useMotion({show,motion: "slide-up",onExitComplete: () => console.log("exited"),animateOnMount: true,});if (!isVisible) return null;return (<div ref={ref as React.RefObject<HTMLDivElement>}>Content</div>);}
| Option | Type | Default | Description |
|---|---|---|---|
| show | boolean | true | Toggle enter/exit |
| motion | MotionPreset | MotionConfig | — | Animation config |
| onExitComplete | () => void | — | Called after exit finishes |
| animateOnMount | boolean | true | Run enter on initial mount |
Pre-wrapped Motion* components
Ready-to-use variants. Import and use as drop-in replacements — all accept the original props plus motion.
import {// Overlay — open/close animatedMotionModal, MotionDrawer, MotionSheet, MotionDialog,MotionPopover, MotionHoverCard,// Feedback — open drivenMotionToast, MotionSnackbar, MotionBannerAlert,// Data Display — mount triggeredMotionCard, MotionAlert,} from "veloria-ui/motion";// Modal — scale from centre<MotionModal motion="fade-scale" open={open} onOpenChange={setOpen} title="Confirm" />// Drawer from bottom<MotionDrawer motion="slide-up" open={open} onOpenChange={setOpen} side="bottom" />// Card — reveal on page load<MotionCard motion={{ preset: "fade-up", delay: 150, duration: 300 }}>Dashboard card</MotionCard>
Stagger
Animate list items one after another with increasing delays using the stagger() utility.
import { Animated, stagger } from "veloria-ui/motion";// stagger(index, stepMs, baseDelayMs){items.map((item, i) => (<Animatedkey={item.id}motion={{ preset: "fade-up", delay: stagger(i, 60) }}><Card>{item.name}</Card></Animated>))}// item 0 → 0ms, item 1 → 60ms, item 2 → 120ms…
MotionConfig — fine-grained control
<Animatedshow={isVisible}motion={{preset: "fade-up", // MotionPreset — default "fade"duration: 300, // ms — default 220delay: 150, // ms — default 0easing: "ease-out", // or "spring", "bounce", custom cubic-bezierdistance: 24, // px — used by translate-based presets, default 16exitReverse: true, // reverse enter frames for exit, default true// Override frames entirely:enterKeyframes: [{ opacity: 0 }, { opacity: 1 }],exitKeyframes: [{ opacity: 1 }, { opacity: 0 }],}}>content</Animated>
Duration constants
import { DURATIONS } from "veloria-ui/motion";DURATIONS.instant // 120msDURATIONS.fast // 220ms ← defaultDURATIONS.normal // 300msDURATIONS.slow // 450msDURATIONS.dramatic // 600ms
prefers-reduced-motion
The motion system automatically detects the OS accessibility setting. When prefers-reduced-motion: reduce is set, all animations are skipped and elements appear/disappear instantly. You can also explicitly disable animations by passing motion="none".