Motion

A small, opinionated motion system: two kinds of curves, five CSS utilities and four spring presets. That's all you need.

Principles

  • Fast in, faster out. Entrances take 200–400ms, exits half that. Nobody waits for a menu to leave.
  • Origin matters. Popovers grow from their trigger, sheets come from their edge, toasts from their corner.
  • Springs for direct manipulation. Anything that follows a pointer or swaps position uses a spring so it can be interrupted mid-flight.
  • CSS when possible. Radix exposes data-state; keyframes do the rest with zero JavaScript.

Curves

Hit play. The difference between linear and expo is the difference between “works” and “feels good”.

Easing curves
linear

Robotic. Avoid for UI.

ease-out-expo

Enters fast, settles softly. Overlays, menus.

ease-spring

CSS linear() spring. Playful accents.

spring.snappy

Motion spring. Indicators, toggles.

--ease-out-expo: cubic-bezier(0.16, 1, 0.3, 1);
--ease-out-quart: cubic-bezier(0.25, 1, 0.5, 1);
--ease-in-quart: cubic-bezier(0.5, 0, 0.75, 0);
--ease-spring: linear(0, 0.009, 0.035 2.1%, 0.141 4.4%, …, 1);

They're regular Tailwind utilities: ease-out-expo, ease-spring…

CSS utilities

Drop these on any Radix content element. They read data-state and data-side and handle enter and exit.

pl-pop

Menus, popovers, tooltips, hover cards. Scales from the trigger with a 4px nudge away from it.

pl-overlay

Fades backdrops in and out.

pl-dialog

Centered modals: rises, unblurs and scales into place.

pl-slide

Sheets. Set data-side to top, right, bottom or left.

pl-collapse

Accordion and collapsible content, animating to the measured height.

<PopoverPrimitive.Content
  className="pl-pop origin-(--radix-popover-content-transform-origin) …"
/>

Springs

Components that use Motion import shared presets from lib/motion.ts. Tune them once, and the whole library follows.

import { motion } from "motion/react"
import { spring } from "@/lib/motion"

<motion.span layoutId="indicator" transition={spring.snappy} />

Reduced motion

The theme collapses every CSS animation and transition when the user asks for reduced motion. For Motion components, wrap your app in MotionConfig to do the same:

import { MotionConfig } from "motion/react"

<MotionConfig reducedMotion="user">{children}</MotionConfig>