# Motion

Easing curves, durations, and reduced-motion rules for state changes and overlays.

## Overview

Motion in Tempo confirms that something responded. Every interactive element animates its state changes, and every overlay animates in and out. Motion stays short, eases out, and never makes you wait to act.

Components already follow these rules. Apply them to anything you build with `style`.

## Easing

Web adds three curves to Zyzz's variables as `ease.*` tokens. Set them with `transitionTimingFunction`, such as `transitionTimingFunction: 'snappy'`, instead of repeating `cubic-bezier` values.

| Token            | Curve                               | Use for                                                                                 |
| ---------------- | ----------------------------------- | --------------------------------------------------------------------------------------- |
| `ease.snappy`    | `cubic-bezier(0.23, 1, 0.32, 1)`    | State changes: hover, focus outlines, presses, popovers, and menus                      |
| `ease.overshoot` | `cubic-bezier(0.34, 1.36, 0.64, 1)` | Physical feedback: `Switch` thumbs, checkbox and radio indicators, copy confirmations   |
| `ease.spring`    | `cubic-bezier(0.32, 0.72, 0, 1)`    | Moving surfaces and indicators: `Dialog`, `Drawer`, `Toast`, `Tabs`, `Accordion` panels |

`snappy` is a quint ease-out, so it moves most in the first frames and settles gently. `overshoot` passes its target slightly and returns. `spring` decelerates over a longer tail, which suits larger travel.

```tsx
import { Button, style, variants } from '@tempoxyz/ds'
import { useState } from 'react'

export const fill = true
const curves = [
  ['snappy', 'State changes and popups'],
  ['overshoot', 'Presses and toggle thumbs'],
  ['spring', 'Dialogs, drawers, and toasts'],
] as const

export function Demo() {
  const [moved, setMoved] = useState(false)

  return (
    <div data-moved={moved || undefined} {...styles.root()}>
      {curves.map(([curve, use]) => (
        <div key={curve} {...styles.row()}>
          <div {...styles.labels()}>
            <code {...styles.token()}>ease.{curve}</code>
            <span {...styles.use()}>{use}</span>
          </div>
          <div {...styles.track()}>
            <span {...styles.dot({ curve })} />
          </div>
        </div>
      ))}
      <Button onClick={() => setMoved(!moved)} size="small" variant="secondary">
        {moved ? 'Move back' : 'Move'}
      </Button>
    </div>
  )
}

namespace styles {
  export const dot = variants({
    base: {
      backgroundColor: 'foreground',
      borderRadius: 'full',
      display: 'block',
      height: '6',
      transitionDuration: '480ms',
      transitionProperty: 'transform',
      width: '6',
      selectors: {
        '[data-moved] &': { transform: 'translateX(calc(100cqw - 1.5rem)) !custom' },
      },
      // Reduced motion keeps the end state and drops the movement between.
      '@media (prefers-reduced-motion: reduce)': { transitionDuration: '0ms' },
    },
    variants: {
      curve: {
        overshoot: { transitionTimingFunction: 'overshoot' },
        snappy: { transitionTimingFunction: 'snappy' },
        spring: { transitionTimingFunction: 'spring' },
      },
    },
  })

  export const labels = style({ display: 'grid', gap: '1' })

  export const root = style({ display: 'grid', gap: '5', justifyItems: 'start' })

  export const row = style({
    alignItems: 'center',
    columnGap: '4',
    display: 'grid',
    gridTemplateColumns: 'minmax(0, 11rem) minmax(0, 1fr) !custom',
    width: '100% !custom',
  })

  export const token = style({ color: 'foreground', typography: 'label.13.mono' })

  export const track = style({
    backgroundColor: 'grayAlpha.100',
    borderRadius: 'full',
    containerType: 'inline-size !custom',
    padding: '1',
  })

  export const use = style({ color: 'gray.900', typography: 'copy.13' })
}
```

[View example source](https://github.com/tempoxyz/ds/blob/main/docs/src/examples/demos/Foundations/Easing.tsx)

The example runs at 480ms so you can compare the curves. Real state changes run faster, as the next table shows.

Zyzz's `ease.in`, `ease.out`, and `ease.in-out` remain available, but components don't use them. Inside a keyframe `animation` shorthand, read a curve from `vars`, such as `vars.ease.overshoot`.

## Durations

| Change                          | Enter | Exit         | Curve                 | Components                              |
| ------------------------------- | ----- | ------------ | --------------------- | --------------------------------------- |
| Press                           | 100ms | 160ms        | `snappy`              | `Button` scales to 0.97                 |
| Menu and list item highlight    | 120ms | 120ms        | `snappy`              | `Menu`, `Select`, `Combobox` items      |
| Hover, focus, and color         | 160ms | 160ms        | `snappy`              | `Button`, `Surface`, `Tabs`, `Checkbox` |
| Check indicators                | 200ms | 200ms        | `overshoot`           | `Checkbox` and `RadioGroup` indicators  |
| Thumbs and selection indicators | 240ms | 240ms        | `overshoot`, `spring` | `Switch` thumb, `Tabs` indicator        |
| Tooltip                         | 160ms | 100ms        | `snappy`              | `Tooltip`                               |
| Popover and menu                | 200ms | 140ms        | `snappy`              | `Popover`, `Menu`                       |
| Select and combobox list        | 240ms | 150ms        | `spring`, `snappy`    | `Select`, `Combobox`                    |
| Expanding content               | 240ms | 180ms        | `spring`              | `Accordion` panels                      |
| Dialog                          | 280ms | 180ms        | `spring`              | `Dialog`, `AlertDialog`                 |
| Drawer and toast                | 320ms | 200 to 240ms | `spring`              | `Drawer`, `Toast`                       |

Micro interactions take 100 to 200ms. Overlays take 200 to 320ms to enter and leave faster than they arrived, so a closing popup never holds the screen.

## Guidelines

### Animate Every State Change

Hover, press, focus-visible outlines, checked and selected states, disabled, and validation each get a transition. A control that snaps between states looks unresponsive, even when it isn't.

Animate overlays in and out, along with toasts, expanding content, and changing values. Start popovers and menus from their trigger with `transform-origin`, a 0.96 scale, and an opacity fade.

### Use Transitions for State, Keyframes for Feedback

Write state changes as CSS transitions. A transition reverses from wherever it is when the state flips back, so a quick hover in and out never jumps.

Keep keyframes for one-shot feedback. `Input` and `OtpField` nudge sideways on an invalid value over 360ms, and `CopyButton` pops its check mark over 240ms.

### Animate Transform and Opacity

Animate `transform`, `opacity`, colors, shadows, and outlines. They don't trigger layout, so they stay smooth on long tables and busy dashboards.

**Do:** Scale a pressed control to 0.97 or 0.98 with `transform` and `ease.snappy`.

**Don't:** Animate `width`, `height`, or `margin` when a transform produces the same effect.

Animate layout only when nothing else works. `Switch` stretches its thumb's width while pressed, since scaling would distort its round ends, and `Toast` animates `height` as its stack collapses.

### Ease Out, Never Linear

Entering and responding motion uses an ease-out curve, so the response starts at full speed. Don't use `linear` or the browser's default `ease` for interface motion.

Continuous indicators are the exception. `Spinner` rotates at a constant rate, since easing would make it pulse.

### Keep Overshoot Rare

Use `ease.overshoot` where physical feedback helps, such as a toggle thumb or a check mark landing. On a dialog or a menu, overshoot reads as wobble.

### Never Delay Input

Controls respond on the first frame of a press. Don't wait for an animation to finish before running an action, and don't block the next interaction while an exit animation plays.

**Do:** Send the payment on press and animate the result in as it arrives.

**Don't:** Hold an action until a button's animation completes.

## Authoring Motion

Pair each transition with a reduced-motion override that removes movement and keeps color changes. Components follow this shape.

```tsx
import { style } from '@tempoxyz/ds'

namespace styles {
  export const row = style({
    transitionDuration: '160ms',
    transitionProperty: 'background-color, transform',
    transitionTimingFunction: 'snappy',
    selectors: {
      '&:active': { transform: 'scale(0.98) !custom' },
      '&:hover': { backgroundColor: 'grayAlpha.100' },
    },
    '@media (prefers-reduced-motion: reduce)': {
      transitionProperty: 'background-color',
      selectors: { '&:active': { transform: 'none !custom' } },
    },
  })
}
```

## Reduced Motion

Under `prefers-reduced-motion: reduce`, components remove movement and scaling and keep instant changes or brief opacity fades.

* `Button`, `Surface`, and other pressable controls stop scaling on press and keep their color transitions.
* `Popover`, `Menu`, `Dialog`, and `Toast` fade without scaling or sliding. On narrow screens, the `Dialog` sheet fades instead of rising.
* `Spinner` stops rotating, `Skeleton` stops its sweep, and `LoadingDots` stop pulsing.
* `CopyButton` skips its pop, and invalid fields skip their nudge.

Follow the same rule in your own styles. Keep the end state, drop the travel, and never hide information inside an animation that reduced motion removes.

## Accessibility

* Respect `prefers-reduced-motion` on Web and Reduce Motion on iOS and Android. Remove movement, scaling, and parallax, and keep opacity fades short.
* Don't flash anything more than three times per second.
* Give looping motion an end or a way to stop it. `Spinner` and `Skeleton` stop when the content they stand in for arrives.
* Never convey information through motion alone. Pair a nudge on an invalid field with a `Field.Error` message.

## Related

* [Loading](/docs/patterns/loading) for spinners, skeletons, and progress
* [Feedback and Status](/docs/patterns/feedback-and-status) for toasts and confirmations
* [Accessibility](/docs/foundations/accessibility) for reduced motion and other preferences
