# How it works

Three mechanisms behind one attribute: keyframes for Radix, transitions for Base UI, and native open state for dialog and popover.

Two mechanisms sit behind one attribute, and you never pick between them. The host's own state
attributes decide, because they are what is actually in the DOM when the component opens. Four
hosts, and the split is uneven: only Radix needs keyframes.

## Hosts and mechanisms

| Mechanism | Host | State it writes | Why |
| --- | --- | --- | --- |
| Keyframes | Radix UI | `data-state="open" \| "closed"` | Presence holds a closing node until animationend, so a keyframe can finish. |
| Transitions | Base UI | `data-starting-style / data-ending-style` | Preferred where available: an interrupted transition resolves, where a keyframe restarts. |
| Transitions | Native dialog and popover | `[open] / :popover-open` | No library involved. display and overlay transition under allow-discrete. |
| Transitions | Plain elements | `[hidden]` | The floor. Any element that toggles hidden, with no component library adopted. |

## Why the mechanism differs

An exit animation needs the element to still exist while it plays. Radix keeps it mounted and
announces the closed state, so keyframes work. Base UI and the platform give you a start and an
end state to interpolate between, so transitions work and are the better tool: they interrupt
cleanly when someone opens and closes the same thing quickly, where a keyframe restarts.

## Do not fight it

Never add Framer Motion or Motion to an element carrying `data-motion`. Both write
`transform`, so the two take turns overwriting `translate` and `scale` and the result reads
as a stutter rather than as a bug. Pick one owner for a given element.

```tsx
{/* Wrong: two owners for one transform */}
<motion.div animate={{ scale: 1 }} data-motion="scale-fade" />

{/* Right: the host signals state, easeful animates it */}
<Dialog.Content data-motion="scale-fade" />
```
