--- title: Animation description: The headless layer stays animation-free; motion lives in the styled wrappers via data attributes and render props. --- The package ships no animation library. Motion is presentation, so it belongs in the styled layer — which keeps the package's peer surface to React alone and lets you swap Motion for CSS, or another library, without touching behavior. ## Two mechanisms **Data attributes for state.** Headless parts expose their state as `data-*` attributes — `data-state="open|closed"`, `data-open`, `data-highlighted`, `data-streaming`, `data-media-type`, and so on. The styled layer animates off them, in CSS or by reading them, with no JavaScript coordination: ```tsx // The shimmer keyframes run purely from the primitive's CSS variables. ``` **State hooks + render props for enter/exit.** Where a wrapper needs true enter/exit choreography (Motion's `AnimatePresence`, layout transitions), it reads the primitive's state through a hook and owns the animated element itself. The thread's scroll button is the pattern: the primitive exposes `isAtBottom` via `useThread`, and the wrapper wraps a `motion.div` in `AnimatePresence`: ```tsx const { isAtBottom, scrollToBottom } = useThread(); {!isAtBottom && ( )} ; ``` ## What this buys you Because the primitive never mounts a motion element, restyling is safe: you can delete the animation, change its curve, or replace the library, and the behavior — when the button should show, when the panel is open — is unchanged. Update the package for behavior fixes; own the motion in your copy of the wrapper.