# Motion showcase

_Box Kite 2.1.0 · a markdown copy of https://www.box-kite.dev/motion/_

Six pieces of motion, each one a prop. Every effect below is run by the browser — there is no animation library here, and nothing on this page measures a layout or ticks a frame.

## Four animations that ship with the engine

Instead of a keyframes block per project, and a media query to stop it.

`animation` takes one of four names, and their `@keyframes` come with the engine — written into the stylesheet the first time something asks for one, so the three you did not use cost nothing. Each preset's duration is a multiple of `--transitionTime`, the variable the base stylesheet zeroes under `prefers-reduced-motion`, so all four stop themselves for a reader who asked for less motion. There is no opt-in to forget.

**spin, pulse, bounce, ping**

```jsx
<Flex
  gap={10}
  ai="center"
  jc="center"
  flexWrap="wrap"
  py={6}
>
  <Icon size={10} color="sky-500" animation="spin">
    <LoaderCircle />
  </Icon>
  <Box
    width={14}
    height={14}
    borderRadius={3}
    bgColor="violet-500"
    animation="pulse"
  />
  <Box
    width={14}
    height={14}
    borderRadius={7}
    bgColor="emerald-500"
    animation="bounce"
  />
  <Box
    width={14}
    height={14}
    borderRadius={7}
    bgColor="rose-500"
    animation="ping"
  />
</Flex>
```

## Spring physics, tuned live, running as CSS

Instead of a physics loop on the main thread for as long as the motion lasts.

A spring is normally a loop: a physics step every frame, on the main thread, for as long as the motion lasts. `Box.spring()` samples the same oscillator once into a `linear()` curve — which both timing-function props already take — so what reaches the browser is a value, and the browser runs it on the compositor. Drag the dials and throw the card: the curve under them is the one the CSS receives.

**Box.spring()**

```jsx
const { easing, duration } = Box.spring({ stiffness: 180, damping: 20, mass: 1 });

<Box
  transition="transform"
  transitionTimingFunction={easing}
  transitionDuration={duration}
  translateX={thrown ? 44 : 0}
/>
```

The four curves on the right are sampled once, on first use, and kept — `transitionTimingFunction="spring-bouncy"` with `transitionDuration="spring-bouncy"` is the whole of it, and a spring is a curve _and_ a settling time, which is why the name goes on both props. Because the curve is a value, two elements given the same spring share one class.

## A panel that animates both ways without unmounting

Instead of an animation library holding the node open until its exit finishes.

`startingStyle` is what a just-mounted element starts _from_, and `transitionBehavior="allow-discrete"` is what flips `display` at the _end_ of the transition rather than the beginning — so the panel below animates in and out without ever leaving the DOM, and without a library holding it open. `<Dialog>` and `<Popover>` are this same pair, already wired.

**startingStyle + allow-discrete**

```jsx
<Box
  display={open ? 'block' : 'none'}
  opacity={open ? 1 : 0}
  translateY={open ? 0 : -2}
  transition="all"
  transitionBehavior="allow-discrete"
  startingStyle={{ opacity: 0, translateY: -2 }}
>
  Both directions, one element.
</Box>
```

## A chart that draws itself as it arrives

Instead of an IntersectionObserver, a scroll listener and a requestAnimationFrame.

`animationTimeline` takes an animation's progress off a scroll position instead of a clock: `scroll()` is the nearest scrollport's progress, `view()` this element's own pass across it. The bar at the top of the panel and the four cards below it are one declaration each — no `IntersectionObserver`, no scroll listener, no `requestAnimationFrame` and no state. Scroll inside the panel.

**scroll() and view()**

```jsx
Box.keyframes({
  progress: { from: { scale: 0 }, to: { scale: 1 } },
  reveal: { from: { opacity: 0, translateY: 5 }, to: { opacity: 1, translateY: 0 } },
});

<Box height={64} overflow="auto">
  <Box position="sticky" top={0} height={1} bgColor="sky-500" css={{ transformOrigin: 'left' }}
    animationName="progress" animationTimeline="scroll()" animationFillMode="both"
    motionReduce={{ animation: 'none' }} />

  <Box animationName="reveal" animationTimeline="view()" animationRange="entry 0% entry 60%"
    animationFillMode="both" motionReduce={{ animation: 'none' }}>
    <Sparkline data={trail} variant="area" />
  </Box>
</Box>
```

This is the one kind of motion here that does **not** stop itself under `prefers-reduced-motion`: a scroll-driven animation has no duration, so the variable that zeroes every other one cannot reach it. `motionReduce={{ animation: 'none' }}` is on every element above, and it is not optional. Where the browser has no timelines the _declaration_ is dropped rather than the animation, so end the sequence where the element belongs and the degradation is "already arrived".

## A layout change the browser animates between

Instead of measuring both layouts and interpolating the difference by hand (FLIP).

A view transition screenshots the page, runs your update, screenshots again and animates between the two. Nothing below declares a transition: the card carries a `viewTransitionName`, and that is enough for the browser to move it from where it was to where it is. The alternative is FLIP — measure both layouts, compute the difference, animate it back — which is the code this prop replaces.

**Box.viewTransition()**

```jsx
// flushSync is the point: the browser screenshots the page the moment the
// callback returns, and a setState has not rendered by then.
<Button onClick={() => Box.viewTransition(() => flushSync(() => setWide((on) => !on)))}>
  Rearrange
</Button>

<Box viewTransitionName="showcase-card" width={wide ? '2/3' : '1/3'} />
```

A name has to be unique in the document while the transition runs, so the prop is for the handful a layout has. Reduced motion **skips** the transition and still applies the update — a whole-page cross-fade being exactly the motion the preference is about. This site's own theme toggle is `<Box.Theme viewTransition>`, which is this call with the `flushSync` already inside it.

## An SVG path that draws itself

Instead of a per-frame write to the dash offset of every path on screen.

A keyframe's steps are Box props, so a sequence can animate anything a prop can set — including the SVG lengths. A path draws itself by animating `strokeDashoffset` from its own `strokeDasharray` down to zero: one dash longer than the line, slid out of the way. Nothing calls `getTotalLength()` — a dash with room to spare covers any path shorter than it, which is why the ring beside it takes the very same two numbers.

**Drawing a path**

```jsx
Box.keyframes({ draw: { from: { strokeDashoffset: 320 }, to: { strokeDashoffset: 0 } } });

<Path d="M4,44 C24,44 28,8 48,8 …" fill="none" stroke="sky-500" strokeWidth={3}
  strokeDasharray={320} animationName="draw" animationDuration={1600}
  animationTimingFunction="ease-in-out" animationFillMode="both" />
```

---

_Every page: https://www.box-kite.dev/llms.txt · every prop, measured: https://www.box-kite.dev/props.md_

