# React Animation

You are working in a React 19 project with TypeScript and Tailwind CSS v4. Animate with CSS first, and with Motion (the `motion` package, imported from `motion/react`) where CSS can't do the job. Write code for the versions in `package.json`: Motion is the library once called Framer Motion, and React 19.3 ships View Transitions.

## Before you write code

- Check `package.json`: `motion` (v12) or an older `framer-motion`. Never install both; the API is the same, only the imports change. Check that Tailwind is v4 (theme in CSS, no `tailwind.config.js`).
- Read the global stylesheet for existing `--animate-*`, duration and easing tokens, and reuse them.
- Check where overlays come from (Base UI, Radix, React Aria): each exposes its own enter and exit hooks.
- For route transitions in Next.js, read `node_modules/next/dist/docs/01-app/02-guides/view-transitions.md`.

## Choose the tool

- CSS: hover, focus and press states, color, small toggles, entry of new elements, spinners, and the open and close of headless overlays. No JavaScript, and it works before hydration.
- Motion: exits of elements React unmounts, layout changes (size, position, reordering, shared elements), drag and springs that follow input, scroll-linked values and sequences.
- React `<ViewTransition>`: route changes, Suspense reveals and swapping whole regions.
- Animate only `transform` (`x`, `y`, `scale`, `rotate`), `opacity`, and sparingly `filter`: they run on the compositor at 60fps. Never animate `width`, `height`, `top`, `left` or `margin` to move things; use Motion's `layout` for size and position changes.
- UI motion is short: 150 to 300 ms for small elements, up to 500 ms for large ones, exits faster than entries. No motion on every keystroke or on page load for content people came to read.

## CSS with Tailwind CSS v4

- Transition only what changes: `transition-opacity`, `transition-colors`, `transition-transform` (in v4 it covers the `translate`, `scale` and `rotate` properties the utilities set), or `transition-[opacity,translate]`, with `duration-200 ease-out`. Avoid `transition-all`.
- Entry without JavaScript: the `starting:` variant (`@starting-style`), as in `transition-opacity starting:opacity-0`. For elements shown and hidden with `display` (`[popover]`, `<dialog>`, `hidden`), use a transition that includes `display` (v4's plain `transition` does) plus `transition-discrete`, so the exit runs before `display: none`.
- Custom keyframes are theme tokens in CSS:

```css
@theme {
  --animate-fade-up: fade-up 200ms ease-out;
  @keyframes fade-up {
    from { opacity: 0; translate: 0 8px; }
  }
}
```

- Headless overlays wait for your CSS before unmounting. Base UI: style `data-starting-style` and `data-ending-style` (`data-starting-style:opacity-0 data-ending-style:opacity-0`). Radix: `data-state="open" | "closed"`, and it waits only for keyframe animations on exit, not transitions. React Aria: `data-entering` and `data-exiting`.

## Motion

```tsx
"use client";

import { AnimatePresence, motion } from "motion/react";
import type { ReactNode } from "react";

export function Reveal({ open, children }: { open: boolean; children: ReactNode }) {
  return (
    <AnimatePresence initial={false}>
      {open && (
        <motion.div
          key="panel"
          initial={{ opacity: 0, y: -4 }}
          animate={{ opacity: 1, y: 0 }}
          exit={{ opacity: 0, y: -4, transition: { duration: 0.15 } }}
          transition={{ type: "spring", visualDuration: 0.25, bounce: 0.1 }}
        >
          {children}
        </motion.div>
      )}
    </AnimatePresence>
  );
}
```

- Motion components live in Client Components. In a Server Component, `import * as motion from "motion/react-client"` for a `motion.div` with `initial` and `animate` only.
- Springs for movement (`type: "spring"` with `visualDuration` and `bounce`, or `stiffness` and `damping`); tweens (`duration`, `ease`) for opacity and color. `initial={false}` skips the first-render animation.
- Variants coordinate a tree: children inherit the parent's `initial` and `animate` labels. Stagger from the parent's `transition` (`delayChildren: stagger(0.05)`, or `staggerChildren` in older code).
- Gestures: `whileHover`, `whileTap`, `whileFocus`, `drag` with `dragConstraints`, and `whileInView` with `viewport={{ once: true, amount: 0.3 }}`. They are visual: keep the action in `onClick` on a real button or link.
- Wrap custom components with `motion.create(Component)`; the component passes `ref` (a plain prop in React 19) to its DOM node.

## Exits that actually run

- `AnimatePresence` stays mounted and the condition goes inside it. Each direct child has a stable, unique `key` (an id, never an index); changing the key exits the old element and enters the new one, which is how you animate steps and slides.
- The `exit` must be on the motion element that is the direct child, or on the root of the child component.
- `mode="wait"` finishes the exit before the entry (swapping one view for another); `mode="popLayout"` takes the exiting element out of the flow so siblings with `layout` close the gap (lists); `sync` is the default.
- For lists, key items by id, add `layout` to each item and `initial={false}` on `AnimatePresence` so the first render doesn't animate.
- With headless overlays, one system owns the exit. Prefer the CSS hooks above. To use Motion instead, control `open`, keep the content mounted in the library (`keepMounted` in Base UI, `forceMount` in Radix) and let `AnimatePresence` decide when it leaves. Never wait for an exit with `setTimeout`: use `onExitComplete`.

## Layout, scroll and sequences

- `layout` animates size and position with transforms. Use `layout="position"` on text that shouldn't stretch, and set `borderRadius` and `boxShadow` in `style` so Motion corrects them while scaling.
- `layoutId` makes a shared element (a tab indicator, a card opening into a dialog): render one element per id at a time and swap which one is mounted. `LayoutGroup` lets siblings that render separately animate together; give it an `id` when the component appears more than once. Add `layoutScroll` to scrolling containers of layout children.
- Scroll: `useScroll({ target: ref, offset: ["start end", "end start"] })` returns `scrollYProgress` (0 to 1); map it with `useTransform(scrollYProgress, [0, 1], [0, -80])`, smooth it with `useSpring`, and bind it through `style`. Motion values update without re-rendering: never mirror them into state, and use `useMotionValueEvent` for side effects.
- Sequences: `const [scope, animate] = useAnimate()`, `ref={scope}`, then `await animate("li", { opacity: 1 }, { delay: stagger(0.05) })` from an effect or a handler. Selectors are scoped, and animations stop when the component unmounts.

## Bundle size

Large trees of motion components cost bundle size. Use `LazyMotion` with `m`:

```tsx
"use client";

import { domAnimation, LazyMotion } from "motion/react";
import * as m from "motion/react-m";
import type { ReactNode } from "react";

export function MotionShell({ children }: { children: ReactNode }) {
  return <LazyMotion features={domAnimation} strict>{children}</LazyMotion>;
}
// below it: <m.div animate={{ opacity: 1 }} /> instead of motion.div
```

`domMax` adds layout and drag; `features` also takes a function returning `import("./motion-features").then((mod) => mod.default)` to load them after first paint. `strict` throws if a full `motion.*` component slips in.

## Reduced motion

- Wrap the app once in `<MotionConfig reducedMotion="user">`: when the system asks for less motion, Motion drops transform and layout animations and keeps opacity and color.
- `useReducedMotion()` for the rest: swap slides for fades, stop autoplay and parallax.
- In CSS, `motion-safe:` opts movement in and `motion-reduce:` takes it out (`motion-reduce:transition-none`, `motion-reduce:animate-none`).
- Anything that moves for more than five seconds gets a pause control. Set `aria` states to the end state at once, not when the animation ends, and never move a focused element out from under the user.

## View Transitions

- `import { ViewTransition } from "react"`. It animates only updates inside `startTransition`, `useDeferredValue`, Suspense reveals and framework navigations, never a plain `setState`.
- The same `name` on two elements morphs one into the other. `enter`, `exit`, `share`, `update` and `default` take a class (or an object keyed by transition type, set with `addTransitionType`), styled with `::view-transition-old(.slide)` and `::view-transition-new(.slide)`. Give named elements `default="none"` so they don't animate on unrelated transitions, and keep `share` explicit with it.
- Next.js 16 needs no config: tag navigations with `<Link transitionTypes={["nav-forward"]}>` or `router.push(href, { transitionTypes })`, and put enter and exit wrappers in `page.tsx`, not layouts, which never unmount.
- Add `::view-transition { pointer-events: none; }`, and zero `::view-transition-*` animation durations under `prefers-reduced-motion: reduce`. Don't run a view transition and a Motion layout animation on the same element.

## Do not

- Do not import from `framer-motion` in new code, or write `motion(Component)` (now `motion.create`), `AnimateSharedLayout` (now `layoutId` and `LayoutGroup`) or `exitBeforeEnter` (now `mode="wait"`).
- Do not put `AnimatePresence` inside the condition, key its children by index, or animate an element both with CSS classes and Motion.
- Do not add `will-change` everywhere or animate layout properties for movement.
- Do not set React state on every animation frame or scroll event.

## Before you finish

- Run the type checker.
- Record a Performance trace with 4x CPU throttling: transform and opacity animations show no layout or paint per frame.
- Emulate `prefers-reduced-motion: reduce` in the browser's rendering tools and check every animation you touched.
- Open and close each overlay fast and repeatedly: exits finish, nothing stays stuck half-visible, and focus returns where it should.
