# Motion recipes

> A practical catalog of lattice-ui's ready-made motion configs and how to override the transition prop on any animatable component part.

Source: https://docs.astra-void.xyz/lattice-ui/guides/motion-recipes/

Every animatable Lattice part takes a typed `transition` prop — `Dialog.Content`, `Popover.Content`, `Checkbox.Indicator`, `Progress.Indicator`, and friends all expose one. You almost never hand-author the config that goes into it: `@lattice-ui/react-motion` ships **recipes**, small factory functions that return a tuned config with the correct timing and target contract baked in. This page is the practical catalog — which recipes exist, which parts they suit, and how to tune one.

> **Presence motion is opt-in**
>
> Since 0.7.0 no primitive ships default **presence** motion. A `Content` or `Indicator` part with no `transition` still mounts, unmounts and holds its content until an exit finishes — it just does so instantly.
>
> **Response** motion that follows a computed value is the exception, because there the motion *is* the behavior: `Progress.Indicator` still settles its fill over 0.12s by default, and the slider thumb, slider range and switch thumb follow their values with recipes you cannot replace. For those, `transition` overrides a default; everywhere else it supplies one.

For the concepts behind all of this — presence vs response vs feedback, target contracts, the hooks themselves — read [Presence and motion](https://docs.astra-void.xyz/lattice-ui/guides/presence-and-motion.md) first. Full signatures live in the [Motion reference](https://docs.astra-void.xyz/lattice-ui/reference/motion.md).

## Which config does a part take?

The `transition` prop's type tells you what kind of motion the part runs:

- **`PresenceMotionConfig`** — reveal/exit motion for parts that mount and unmount. It has `initial`, `reveal`, and `exit` steps. `Dialog.Content`, `Popover.Content`, `Tooltip.Content`, `Menu.Content`, `Tabs.Content`, `Accordion.Content`, `Checkbox.Indicator`, and `RadioGroup.Indicator` all take one.
- **`ResponseMotionConfig`** — settling motion for parts that stay mounted and ease between states. It has a single `settle` intent. `Progress.Indicator` and `Toast.Root` take one.

The rule of thumb: if the part appears and disappears, it wants a presence config; if it slides or recolors between values while staying on screen, it wants a response config. Passing the wrong kind is a type error, so you cannot mix them up silently.

> **Not every part takes a transition**
>
> Some parts own presence timing without exposing a `transition` prop. `Dialog.Overlay` is the one you are most likely to hit: an unstyled overlay has nothing to fade, so it takes no transition at all. To animate a dim, style an element inside the overlay (or pass `asChild`) and animate that element yourself.

## Recipe catalog

Presence recipes return a `PresenceMotionConfig`; response recipes return a `ResponseMotionConfig`; feedback recipes return a `FeedbackEffectConfig` for [`useFeedbackEffect`](https://docs.astra-void.xyz/lattice-ui/reference/motion.md) in custom components.

| Recipe | Animates | Pair it with |
| --- | --- | --- |
| `createSurfaceRevealRecipe(offsetY?, duration?)` | Upward slide + `BackgroundTransparency` fade | `Tabs.Content`, `Accordion.Content` |
| `createCanvasGroupRevealRecipe(offsetY?, duration?)` | Upward slide + `GroupTransparency` fade | Your own `canvasgroup`, through `asChild` or `usePresenceMotion` |
| `createOverlayFadeRecipe(duration?)` | Backdrop fade to half transparency | Your own element inside `Dialog.Overlay` |
| `createPopperEntranceRecipe(placement?, distance?, duration?)` | Directional slide-in from the anchor side + fade | `Tooltip.Content`, `Menu.Content`, `ContextMenu.Content`, `Combobox.Content`, `Popover.Content`, `Select.Content` |
| `createCanvasGroupPopperEntranceRecipe(placement?, distance?, duration?)` | Same, via `GroupTransparency` | Your own `canvasgroup`, supplied to an anchored part through `asChild` |
| `createIndicatorRevealRecipe(size, duration?)` | Grow from zero to `size` + fade | `Checkbox.Indicator`, `RadioGroup.Indicator` |
| `createIndicatorSettleRecipe(duration?)` | Size settling on an indicator wrapper | Custom selection indicators |
| `createSliderThumbResponseRecipe(isDragging, duration?)` | Thumb position — instant while dragging, eased on release | Slider thumb |
| `createToggleResponseRecipe(duration?)` | Layout settling for on/off thumbs | Switch-style toggles |
| `createSelectionResponseRecipe(duration?)` | Color/appearance settling on selection | Checkbox, Radio visuals |
| `createFieldResponseRecipe(duration?)` | Calm appearance settling for form fields | Text fields |
| `createProgressResponseRecipe(duration?)` | Fill size settling | `Progress.Indicator` |
| `createToastResponseRecipe(duration?)` | Appearance settling between toast states | `Toast.Root` |
| `createPressFeedbackEffect(duration?)` | Expressive accent on press, calm recover | Buttons, pressables |
| `createFocusAccentEffect(duration?)` | Slower accent on focus, quick recover | Focus rings |

Every recipe also sets the right **target contract** — `createOverlayFadeRecipe` uses an `appearance` target, the reveal and popper recipes use an `offset-wrapper`, `createIndicatorRevealRecipe` a `size-wrapper`. That is a large part of why you should prefer a recipe over a hand-written config: the ownership boundary comes for free.

`Dialog.Content` is the one part with no recipe to reach for by default. It renders a full-screen `Frame`, so a transition can slide it but has no property that fades its descendants — write a `Position`-only config for it and fade the overlay or your own panel elements separately, or pass `asChild` with a `canvasgroup` and use `createCanvasGroupRevealRecipe()`. See [Fading a dialog](https://docs.astra-void.xyz/lattice-ui/components/dialog.md#fading-a-dialog).

## Giving a component a transition

Pass a recipe's result straight to `transition`. Called with no arguments a recipe uses the house tokens — an 8-pixel rise over a 0.12s reveal, with the exit at 0.8x that. Call the same factory with arguments for a longer, larger reveal:

```tsx title="ShopTabs.tsx"
import { createSurfaceRevealRecipe } from "@lattice-ui/react-motion";
import { Tabs } from "@lattice-ui/react-tabs";

const PANEL_REVEAL = createSurfaceRevealRecipe(16, 0.45);

export function ShopTabs() {
  return (
    <Tabs.Root defaultValue="weapons">
      <Tabs.List>
        <Tabs.Trigger value="weapons" />
        <Tabs.Trigger value="armor" />
      </Tabs.List>

      <Tabs.Content value="weapons" transition={PANEL_REVEAL} asChild>
        <frame BackgroundColor3={Color3.fromRGB(24, 26, 32)} Size={UDim2.fromOffset(420, 220)} />
      </Tabs.Content>
      <Tabs.Content value="armor" transition={PANEL_REVEAL} asChild>
        <frame BackgroundColor3={Color3.fromRGB(24, 26, 32)} Size={UDim2.fromOffset(420, 220)} />
      </Tabs.Content>
    </Tabs.Root>
  );
}
```

Every part uses the config you pass **as-is**. Nothing is merged underneath it, so a partial config animates only the steps it defines — and where a part does have a default (`Progress.Indicator`), your config replaces it wholesale rather than layering over it. To vary one step of a recipe, spread it and replace that step; see [Tuning beyond the factory arguments](#tuning-beyond-the-factory-arguments) below.

The same pattern works for indicators. `createIndicatorRevealRecipe` takes the size the indicator should settle at, so it has to match the box you are rendering:

```tsx title="LargeCheckbox.tsx"
import { Checkbox } from "@lattice-ui/react-checkbox";
import { createIndicatorRevealRecipe } from "@lattice-ui/react-motion";

export function LargeCheckbox(props: { checked: boolean; onCheckedChange: (checked: boolean) => void }) {
  return (
    <Checkbox.Root checked={props.checked} onCheckedChange={props.onCheckedChange}>
      <Checkbox.Indicator
        transition={createIndicatorRevealRecipe(UDim2.fromOffset(18, 18), 0.14)}
      />
    </Checkbox.Root>
  );
}
```

Response-driven parts work the same way, just with a response recipe:

```tsx title="SlowProgress.tsx"
import { Progress } from "@lattice-ui/react-progress";
import { createProgressResponseRecipe } from "@lattice-ui/react-motion";

export function SlowProgress(props: { value: number }) {
  return (
    <Progress.Root value={props.value}>
      <Progress.Indicator transition={createProgressResponseRecipe(0.25)} />
    </Progress.Root>
  );
}
```

## Tuning beyond the factory arguments

Recipe factories expose the knobs that are safe to turn — an offset or placement distance in pixels, a `duration` in seconds, a reveal `size`. The **tempo** (`"instant" | "swift" | "steady" | "gentle"`) and **tone** (`"calm" | "responsive" | "expressive"`) of each step are baked in; they are the house style.

When a recipe is almost right, spread its result and override just the step you care about instead of authoring a config from scratch. The recipe's target contract and remaining steps carry over:

```tsx title="Snappy exit, recipe entrance"
import { createSurfaceRevealRecipe, type PresenceMotionConfig } from "@lattice-ui/react-motion";

const base = createSurfaceRevealRecipe(8, 0.3);

const snappyExit: PresenceMotionConfig = {
  ...base,
  exit: {
    values: base.exit!.values,
    intent: { duration: 0.08, tempo: "swift", tone: "calm" },
  },
};

<Tabs.Content value="weapons" transition={snappyExit} />;
```

Two constraints to keep in mind when editing steps:

- **Match the transparency property to the instance.** No primitive renders a `CanvasGroup` any more, so the canvas-group recipes only fit a `canvasgroup` you supply yourself through `asChild`; everything else fades `BackgroundTransparency`, which reaches one instance's own background rather than its children. On a part whose host spans the layer, like `Dialog.Content`, neither fade does what you want.
- **Stay inside the target contract.** If you add `Position` values to a recipe whose target is `appearance`, the write is rejected and reported as a diagnostic. Keep the recipe's `target` when you keep its properties; see [target contracts](https://docs.astra-void.xyz/lattice-ui/guides/presence-and-motion.md#target-contracts-who-owns-the-property).

## Reduced motion is automatic

You do not opt recipes into reduced motion — every hook that consumes them reads the ambient `MotionPolicy`. When motion is disabled, configs collapse to instant property writes: the final values still land, exits still fire `onExitComplete`, and response settling still reaches its target. Your overridden transitions inherit this for free.

The policy disables motion in two cases: you set it explicitly, or the player has Roblox's system reduced-motion setting enabled. `MotionProvider` respects the system setting by default (`respectSystemReducedMotion` defaults to `true`), and `useSystemReducedMotion` / `useMotionPolicy` let you read the resolved state yourself:

```tsx title="App.tsx"
import { MotionProvider, useMotionPolicy } from "@lattice-ui/react-motion";

export function App(props: { forceReducedMotion: boolean; children: React.ReactNode }) {
  return (
    <MotionProvider disableAllMotion={props.forceReducedMotion}>
      {props.children}
    </MotionProvider>
  );
}

function DebugMotionState() {
  const policy = useMotionPolicy();
  return <textlabel Text={policy.disableAllMotion ? "motion off" : "motion on"} Size={UDim2.fromOffset(120, 20)} />;
}
```

`useMotionPolicy` folds the system setting into its result — `disableAllMotion` is `true` whenever the provider disables motion *or* `respectSystemReducedMotion` is on and the player's system setting is enabled. There is no reduced-motion branch to write in your components: build the animated path, and the instant path falls out of the policy.

## Related

- [Presence and motion](https://docs.astra-void.xyz/lattice-ui/guides/presence-and-motion.md) — the concepts: hooks, phases, target contracts
- [Motion reference](https://docs.astra-void.xyz/lattice-ui/reference/motion.md) — full signatures for every recipe, hook, and type
- [Dialog](https://docs.astra-void.xyz/lattice-ui/components/dialog.md)
- [Checkbox](https://docs.astra-void.xyz/lattice-ui/components/checkbox.md)
- [Progress](https://docs.astra-void.xyz/lattice-ui/components/progress.md)
