Motion System: Tailwind UI / Headless UI
Category: Component Library / Design System Explicit, CSS-class-driven, and intentionally neutral — motion defined as transition classes, not JS springs.
Motion Philosophy
Tailwind UI and Headless UI occupy a unique position: they provide motion not as a fixed aesthetic but as a documented, overridable default. The motion system is delivered through CSS transition utility classes applied via the Headless UI Transition component's enterFrom, enterTo, leaveFrom, and leaveTo props. Every transition is explicit, composable, and replaceable. There are no hidden spring configs, no imperative JS animations — only CSS.
This philosophy reflects Tailwind's core value: escape hatches everywhere. The provided transitions are sensible defaults that pair with any design system, from a SaaS dashboard to a marketing site. They use ease-out for entrances and ease-in for exits, with durations of 100–200ms. These values are intentionally middle-of-the-road — fast enough to not feel sluggish, slow enough to not feel broken on older hardware. A team using Tailwind UI can swap any of these class strings for their own in under a minute.
The absence of JS springs is a deliberate choice, not a limitation. By keeping motion in CSS, Headless UI transitions are compatible with server-side rendering, work without a JavaScript animation library, perform on the compositor thread, and are trivially inspectable in browser DevTools. The constraint produces clarity: a developer reading a Headless UI component immediately sees all transition behavior in the className props, with no runtime abstraction to trace through.
Duration Scale
| Token | Value | Tailwind Class | Use |
|---|---|---|---|
| instant | 0ms | duration-0 | Keyboard focus highlight, active selection background |
| fast | 100ms | duration-100 | Tooltip, badge, small dropdown leave transition |
| default | 150ms | duration-150 | Leave transitions: menus, popovers, dialogs (leaveFrom/leaveTo) |
| enter | 200ms | duration-200 | Enter transitions: menus, popovers, dialogs (enterFrom/enterTo) |
| slow | 300ms | duration-300 | Slide-over panels, full-page dialogs, sidebar |
| deliberate | 500ms | duration-500 | Rarely used; page-level transitions in marketing contexts |
Note: Headless UI officially documents duration-200 for enter and duration-150 for leave as the canonical defaults. This asymmetry — enter longer than leave — is the opposite of Linear and Raycast's philosophy, reflecting that Tailwind UI targets a broader context where slightly longer entrances aid orientation for less-experienced users.
Easing
| Token | Curve | Tailwind Class | Use |
|---|---|---|---|
| ease-out | cubic-bezier(0, 0, 0.2, 1) | ease-out | Enter transitions — all enterFrom/enterTo |
| ease-in | cubic-bezier(0.4, 0, 1, 1) | ease-in | Leave transitions — all leaveFrom/leaveTo |
| ease-in-out | cubic-bezier(0.4, 0, 0.2, 1) | ease-in-out | Repositioning, progress indicators |
| linear | linear | ease-linear | Loading spinners, progress bars |
Tailwind's ease-out maps to CSS cubic-bezier(0, 0, 0.2, 1) — a moderate deceleration. It is less aggressive than the developer-tool defaults used by Linear or Raycast, which is appropriate: Tailwind UI components appear in contexts ranging from B2B SaaS to e-commerce, and a violent deceleration can feel jarring on a light-themed marketing site.
Spring Configs (Framer Motion)
Headless UI does not use JS springs natively. When a team integrates Framer Motion with a Tailwind UI design language, the following configs reflect the Tailwind aesthetic:
- Default (matches
ease-out duration-200feel): stiffness: 300, damping: 30, mass: 1 - Gentle (matches
ease-out duration-300feel): stiffness: 200, damping: 26, mass: 1 - Micro (for small interactive feedback): stiffness: 500, damping: 38, mass: 0.8
These are provided as a mapping guide only. The canonical Tailwind UI motion system does not ship spring configs. If a project uses Framer Motion, these values approximate the CSS transition defaults.
Stagger Patterns
Headless UI does not provide a built-in stagger utility. Stagger is implemented manually by consumers using index-based transition-delay utilities:
- List items:
delay-[0ms],delay-[30ms],delay-[60ms]... using arbitrary Tailwind values or a mapped delay scale - Menu items on open: 0ms stagger recommended (no stagger) — menus should appear complete, not sequential
- Cards on page load: 50ms between cards using inline
style={{ transitionDelay:${index * 50}ms}}
The recommendation from Headless UI documentation is to avoid stagger for interactive menus and dropdowns — stagger in a 300ms menu makes keyboard navigation feel broken because the target item may not yet be visible.
Enter / Exit Patterns
All patterns below show the actual Headless UI prop values as used in production Tailwind UI components.
Dropdown Menu (Menu, Listbox)
enter: "transition ease-out duration-200"
enterFrom: "transform opacity-0 scale-95"
enterTo: "transform opacity-100 scale-100"
leave: "transition ease-in duration-150"
leaveFrom: "transform opacity-100 scale-100"
leaveTo: "transform opacity-0 scale-95"
Dialog / Modal
enter: "ease-out duration-300"
enterFrom: "opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
enterTo: "opacity-100 translate-y-0 sm:scale-100"
leave: "ease-in duration-200"
leaveFrom: "opacity-100 translate-y-0 sm:scale-100"
leaveTo: "opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
Backdrop:
enter: "ease-out duration-300"
enterFrom: "opacity-0"
enterTo: "opacity-100"
leave: "ease-in duration-200"
leaveFrom: "opacity-100"
leaveTo: "opacity-0"
Slide-Over Panel
enter: "transform transition ease-in-out duration-500 sm:duration-700"
enterFrom: "translate-x-full"
enterTo: "translate-x-0"
leave: "transform transition ease-in-out duration-500 sm:duration-700"
leaveFrom: "translate-x-0"
leaveTo: "translate-x-full"
Popover / Tooltip
enter: "transition ease-out duration-200"
enterFrom: "opacity-0 translate-y-1"
enterTo: "opacity-100 translate-y-0"
leave: "transition ease-in duration-150"
leaveFrom: "opacity-100 translate-y-0"
leaveTo: "opacity-0 translate-y-1"
Combobox / Select Dropdown
enter: "transition ease-out duration-100"
enterFrom: "transform opacity-0 scale-95"
enterTo: "transform opacity-100 scale-100"
leave: "transition ease-in duration-75"
leaveFrom: "transform opacity-100 scale-100"
leaveTo: "transform opacity-0 scale-95"
Interaction States
- Hover: Defined entirely in CSS via Tailwind
hover:variants. No transition on background color by default — teams addtransition-colors duration-150manually where desired. Headless UI applies anactivedata attribute (data-active) for menu items, used asdata-[active]:bg-gray-100. - Focus: Uses
focus:andfocus-visible:variants. Focus rings appear instantly with no transition. Headless UI's components manage focus automatically; the visual ring is styled viafocus:outline-none focus-visible:ring-2 focus-visible:ring-indigo-500. - Press/Active: No CSS scale transform in Tailwind UI defaults. The active state changes background color only:
active:bg-gray-100. Teams may addactive:scale-95 transition-transformmanually. - Disabled: Opacity via
disabled:opacity-50. No transition on the opacity change — disabled states are instantaneous. - Loading: Not standardized. Common pattern is a CSS spinner:
animate-spinclass applies a 1s linear infinite rotation. Skeleton loaders useanimate-pulse(CSSopacity: 1 → 0.5 → 1over 2s ease-in-out infinite). - Toggle/Switch: The Headless UI
Switchcomponent animates its thumb usingtranslate-x-0→translate-x-5withtransition ease-in-out duration-200. Background color transitions simultaneously viatransition-colors duration-200 ease-in-out.
Rules
- All transition behavior must be visible in the component's className props. Never hide motion in a wrapper component or custom hook that obscures the transition classes from the consumer. Discoverability is a first-class constraint.
- Use
ease-outwithduration-200for enters;ease-inwithduration-150for leaves. These are the documented Headless UI defaults and must be the starting point before any customization. - Honor
prefers-reduced-motionvia Tailwind'smotion-reduce:variant. Applymotion-reduce:transition-noneto all transition utilities andmotion-reduce:transform-noneto all transform-based enter/exit patterns. - CSS transitions only for documented Headless UI components. Introduce Framer Motion or other JS animation libraries only when CSS transitions cannot model the required interaction (e.g., drag-and-drop reordering, complex path animations). Document the decision when doing so.