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-motion rule shortens every animation and transition.

Easing

TokenCurveUse
ease-outcubic-bezier(0.16, 1, 0.3, 1)Enters, reveals, hover. Fast start, long soft landing.
ease-springcubic-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.

UtilityDefinition
animate-fade-upvibe-fade-up 0.6s var(--ease-out) both
animate-fade-invibe-fade-in 0.25s ease-out both
animate-pop-invibe-pop-in 0.22s var(--ease-out) both
animate-marqueevibe-marquee 40s linear infinite
animate-pulse-quietvibe-pulse 1.8s ease-in-out infinite
animate-spin-quietvibe-spin 0.8s linear infinite
animate-orbitvibe-spin 28s linear infinite
animate-shimmervibe-shimmer 3s linear infinite
animate-jellyvibe-jelly 0.9s ease-in-out infinite
animate-blinkvibe-blink 1s steps(2, start) infinite
animate-fade-outvibe-fade-out 0.18s ease-in both
animate-pop-outvibe-pop-out 0.16s ease-in both
animate-slide-in-rightvibe-slide-in-right 0.45s var(--ease-out) both
animate-slide-out-rightvibe-slide-out-right 0.28s ease-in both
animate-slide-in-leftvibe-slide-in-left 0.45s var(--ease-out) both
animate-slide-out-leftvibe-slide-out-left 0.28s ease-in both
animate-slide-in-bottomvibe-slide-in-bottom 0.45s var(--ease-out) both
animate-slide-out-bottomvibe-slide-out-bottom 0.28s ease-in both
animate-grow-xvibe-grow-x 1s var(--ease-out) both
animate-grow-yvibe-grow-y 1s var(--ease-out) both
animate-shrink-xvibe-shrink-x 4s linear forwards
animate-ping-softvibe-ping 2s cubic-bezier(0, 0, 0.2, 1) infinite
animate-floatvibe-float 4.5s ease-in-out infinite
animate-eqvibe-eq 0.9s ease-in-out infinite
animate-tickvibe-tick 0.35s var(--ease-out) both
animate-month-nextvibe-month-next 0.38s var(--ease-out) both
animate-month-prevvibe-month-prev 0.38s var(--ease-out) both
animate-toast-invibe-toast-in 0.45s var(--ease-spring) both
animate-toast-outvibe-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.

InteractionTreatment
Hover (surface)Border strengthens; cards lift 2px and gain shadow-lift.
Hover (button)Rises 1px; filled variants gain shadow-lift.
Pressscale(0.97), returning on release. Icon buttons use 0.9.
Focus2px accent outline (global) or a 3px accent ring on fields.
Toggle onTrack fills with the accent; thumb springs across and stretches under the finger.
Close buttonsRotate 90° on hover.
AppearOverlays pop in (scale + fade); sheets slide from their edge; lists stagger.
DisappearThe mirror of appear, then unmount (see below).
StatusLive 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:

the pattern, as used in dialog.tsx and popover.tsx
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.

tsx
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:

css
@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.