<!-- Protocore Design System — Transition -->
# Transition

- **Category:** Utilities (`utilities`)
- **Slug:** `utilities/transition`
- **Status:** stable
- **Platforms:** web
- **Import:** `import { Transition } from "@protocore/pds";`
- **Docs:** https://pds.protocore.io/components/utilities/transition

> A mount/unmount transition primitive: keeps children mounted through an exit tween, then removes them.

## When to use it

**Transition** is the low-level primitive the library's own overlays are built on. Reach for it when you need to animate something *in and out* of the tree — the exit animation is the hard part, since a naive conditional render rips the element out before it can animate.

It is a headless render-prop: it computes an inline `style` (opacity + transform + the timing) and the current `status`, and you decide what element to put them on. That keeps it composable onto any node without an extra wrapper. For a ready-made loading scrim, use [LoadingOverlay](/feedback/loading-overlay); for portalled surfaces with focus management, prefer [Popover](/overlay/popover) or [Dialog](/overlay/dialog), which handle transitions internally.

## Props

| Prop | Type | Required | Default | Description |
| --- | --- | --- | --- | --- |
| `children` | `(style: CSSProperties, status: TransitionStatus) => ReactNode` | yes | — | Render-prop: receives the inline `style` to spread onto your own element, plus the current transition `status`. Return a single element. |
| `duration` | `number` | no | `200` | Animation duration in ms (exit reuses it unless `exitDuration` is set). |
| `exitDuration` | `number` | no | — | Optional distinct exit duration in ms. Falls back to `duration`. |
| `mounted` | `boolean` | yes | — | When `true` the child is mounted and animated in; when `false` it animates out and unmounts. |
| `onEntered` | `(() => void)` | no | — | Called after the enter animation completes. |
| `onExited` | `(() => void)` | no | — | Called after the exit animation completes and the child has unmounted. |
| `timingFunction` | `string` | no | `var(--pds-ease)` | CSS timing function for the tween. |
| `transition` | `enum` | no | `fade` | Named enter/exit preset. |

## Examples

### Basics

Toggle `mounted` and the child fades in; toggle it off and the child stays mounted through the fade-out, then unmounts. The render-prop hands you a `style` object to spread onto your element.

```tsx
import { useState } from "react";
import { Transition, Button, Stack, Card, Text } from "@protocore/pds";

export default function Basics() {
  const [mounted, setMounted] = useState(false);
  return (
    <Stack gap={4} align="start">
      <Button variant="secondary" size="sm" onClick={() => setMounted((m) => !m)}>
        {mounted ? "Hide" : "Show"}
      </Button>
      <Transition mounted={mounted} transition="fade" duration={200}>
        {(style) => (
          <Card style={style}>
            <Text>Block #4,812,004 finalised — 1,204 transactions.</Text>
          </Card>
        )}
      </Transition>
    </Stack>
  );
}
```

### Presets

Five named presets — `fade`, `slide-up`, `slide-down`, `scale`, and `pop` — each defining an enter/exit opacity and transform. Tune `duration` and `timingFunction` per instance.

```tsx
import { useState } from "react";
import {
  Transition,
  type TransitionPreset,
  Button,
  SegmentedControl,
  Stack,
  Card,
  Text,
} from "@protocore/pds";

const PRESETS: TransitionPreset[] = ["fade", "slide-up", "slide-down", "scale", "pop"];

export default function Presets() {
  const [preset, setPreset] = useState<TransitionPreset>("slide-up");
  const [mounted, setMounted] = useState(true);
  return (
    <Stack gap={4} align="start">
      <SegmentedControl
        aria-label="Transition preset"
        value={preset}
        onValueChange={(v) => setPreset(v as TransitionPreset)}
        items={PRESETS.map((p) => ({ value: p, label: p }))}
      />
      <Button variant="secondary" size="sm" onClick={() => setMounted((m) => !m)}>
        {mounted ? "Play exit" : "Play enter"}
      </Button>
      <div style={{ minHeight: 72 }}>
        <Transition mounted={mounted} transition={preset} duration={240}>
          {(style) => (
            <Card style={style}>
              <Text>Validator 0x8f…c2 rotated its signing key.</Text>
            </Card>
          )}
        </Transition>
      </div>
    </Stack>
  );
}
```

## Do & don't

**Do**

- Spread the provided `style` onto exactly one element, and let Transition own its mounting.
- Use `duration`/`exitDuration` to give exits a slightly different pace when it feels right.
- Reach for it to animate conditional content out of the tree, not just in.
- Pick a preset that matches the surface's origin (slide for edges, scale/pop for anchored popovers).

**Don't**

- Don't also conditionally render the child yourself — Transition manages mount/unmount.
- Don't return multiple root elements from the render-prop; give it a single node.
- Don't hand-build entrance animations for Dialog/Popover/Toast — they already transition.
- Don't rely on the tween for critical information — reduced-motion users get an instant snap.

## Accessibility

**Notes**

- `prefers-reduced-motion: reduce` collapses both the enter and exit durations to zero, so motion-sensitive users get an instant show/hide rather than no content.
- Transition renders no DOM of its own — accessibility (roles, focus, live regions) is the responsibility of the element you apply the style to.
- Because the child stays mounted during exit, any focus you moved into it should be moved out before you set `mounted={false}`.
- The `status` argument (`entering` / `entered` / `exiting`) lets you gate `aria-hidden` or `inert` if the exiting content should leave the a11y tree immediately.

## Related

`loading-overlay`, `popover`, `toast`, `pds-provider`

---

© Protocore. All rights reserved. Use of the Protocore Design System requires prior written authorization from Protocore (contact@protocore.io). These machine-readable materials must not be ingested into ML-training datasets or derivative design systems. See https://pds.protocore.io/legal/
