Customize
Motion
Every component has hover, press and focus feedback, and anything that appears also disappears with an animation. Motion lives in the token file, so copied components animate in your app.
Principles
- Transform and opacity only. They run on the compositor and never trigger layout.
- Fast to respond, slow to settle. Hover and press react in 150–200ms; entrances take 300–600ms with an ease-out curve.
- Motion explains. Things slide from where they came from, and leave toward it.
- Always optional. One
prefers-reduced-motionrule shortens every animation and transition.
Easing
| Token | Curve | Use |
|---|---|---|
| ease-out | cubic-bezier(0.16, 1, 0.3, 1) | Enters, reveals, hover. Fast start, long soft landing. |
| ease-spring | cubic-bezier(0.34, 1.56, 0.64, 1) | Playful overshoot: switch thumbs, toasts, circle menu, stepper. |
Animation utilities
Declared with --animate-* in the token file (so Tailwind generates animate-* classes and includes the keyframes only when used). This table is read from the file.
| Utility | Definition |
|---|---|
| animate-fade-up | vibe-fade-up 0.6s var(--ease-out) both |
| animate-fade-in | vibe-fade-in 0.25s ease-out both |
| animate-pop-in | vibe-pop-in 0.22s var(--ease-out) both |
| animate-marquee | vibe-marquee 40s linear infinite |
| animate-pulse-quiet | vibe-pulse 1.8s ease-in-out infinite |
| animate-spin-quiet | vibe-spin 0.8s linear infinite |
| animate-orbit | vibe-spin 28s linear infinite |
| animate-shimmer | vibe-shimmer 3s linear infinite |
| animate-jelly | vibe-jelly 0.9s ease-in-out infinite |
| animate-blink | vibe-blink 1s steps(2, start) infinite |
| animate-fade-out | vibe-fade-out 0.18s ease-in both |
| animate-pop-out | vibe-pop-out 0.16s ease-in both |
| animate-slide-in-right | vibe-slide-in-right 0.45s var(--ease-out) both |
| animate-slide-out-right | vibe-slide-out-right 0.28s ease-in both |
| animate-slide-in-left | vibe-slide-in-left 0.45s var(--ease-out) both |
| animate-slide-out-left | vibe-slide-out-left 0.28s ease-in both |
| animate-slide-in-bottom | vibe-slide-in-bottom 0.45s var(--ease-out) both |
| animate-slide-out-bottom | vibe-slide-out-bottom 0.28s ease-in both |
| animate-grow-x | vibe-grow-x 1s var(--ease-out) both |
| animate-grow-y | vibe-grow-y 1s var(--ease-out) both |
| animate-shrink-x | vibe-shrink-x 4s linear forwards |
| animate-ping-soft | vibe-ping 2s cubic-bezier(0, 0, 0.2, 1) infinite |
| animate-float | vibe-float 4.5s ease-in-out infinite |
| animate-eq | vibe-eq 0.9s ease-in-out infinite |
| animate-tick | vibe-tick 0.35s var(--ease-out) both |
| animate-month-next | vibe-month-next 0.38s var(--ease-out) both |
| animate-month-prev | vibe-month-prev 0.38s var(--ease-out) both |
| animate-toast-in | vibe-toast-in 0.45s var(--ease-spring) both |
| animate-toast-out | vibe-toast-out 0.28s ease-in both |
Micro-interaction conventions
The same small vocabulary appears everywhere, which is most of why the library feels coherent.
| Interaction | Treatment |
|---|---|
| Hover (surface) | Border strengthens; cards lift 2px and gain shadow-lift. |
| Hover (button) | Rises 1px; filled variants gain shadow-lift. |
| Press | scale(0.97), returning on release. Icon buttons use 0.9. |
| Focus | 2px accent outline (global) or a 3px accent ring on fields. |
| Toggle on | Track fills with the accent; thumb springs across and stretches under the finger. |
| Close buttons | Rotate 90° on hover. |
| Appear | Overlays pop in (scale + fade); sheets slide from their edge; lists stagger. |
| Disappear | The mirror of appear, then unmount (see below). |
| Status | Live dots emit a soft ping; unread dots pulse; skeletons shimmer. |
Enter and exit
CSS can animate things in as they mount, but not out as they unmount. Overlays here keep themselves mounted until the exit animation has finished, with no effects and no animation library:
const [present, setPresent] = React.useState(open)
if (open && !present) setPresent(true) // mount when opening
if (!present) return null
return (
<div
data-state={open ? "open" : "closed"}
onAnimationEnd={(e) => {
// Only our own exit animation, not a child's.
if (e.target === e.currentTarget && !open) setPresent(false)
}}
className="data-[state=open]:animate-pop-in data-[state=closed]:animate-pop-out"
/>
)Pointer-driven effects
Spotlight, tilt, magnetic and dock effects respond to the cursor. They write CSS variables or style properties directly on the element inside the event handler, instead of calling setState, so moving the mouse does not re-render React at 60fps.
onMouseMove={(e) => {
const r = el.getBoundingClientRect()
el.style.setProperty("--x", `${e.clientX - r.left}px`)
el.style.setProperty("--y", `${e.clientY - r.top}px`)
}}
// then in CSS: radial-gradient(400px circle at var(--x) var(--y), …)Independent transform properties
When a component both plays an entrance animation and reacts to the pointer, use the individual scale and translate properties for the pointer part. A running transform animation otherwise overrides inline transform.
Reduced motion
The token file includes a single rule that makes every animation and transition effectively instant when the user asks for less motion:
@media (prefers-reduced-motion: reduce) {
*, *::before, *::after {
animation-duration: 0.01ms !important;
animation-iteration-count: 1 !important;
transition-duration: 0.01ms !important;
scroll-behavior: auto !important;
}
}Because exits wait for animationend, which still fires at 0.01ms, overlays close correctly under reduced motion.
Turning motion down yourself
- Remove an
animate-*class from a component file to take its entrance away. - Shorten every transition by editing the
duration-*values; there are no hidden timings. - Delete the reduced-motion block's counterpart if you want the opposite: motion is never forced on.