Pho Design System

Motion

Every animation in Pho speaks one vocabulary: five springs, three timings, two CSS eases, a handful of distances. Components import these named tokens and add nothing of their own — a new surface that moves should sound like the rest of the site. The grammar in one breath: arrive on the entrance spring, swap through an ease-out fade, travel linearly, press on a spring with mass, follow content on the settle spring.

Click a card and the ball speaks its token.

Import

import {
  SPRING_ENTRANCE,
  FADE_SWAP,
  TRAVEL_LINEAR,
  fadeThroughBlur,
} from "@photon-ai/pho-ui/motion";

Springs

Timings

Fades ease out; displacement is linear. When one move carries both — the condensed bar rising in — spread them per channel:

transition={{ ...FADE_SWAP, y: TRAVEL_LINEAR }}

Presses

Three press springs, by the size of what’s under the pointer. Depth comes from PRESS_SCALE (0.96, small controls) and PRESS_SCALE_ROW (0.97, rows and full-size surfaces).

CSS eases

Where the animation lives in a stylesheet (Base UI popups, the toast stack), the curve is a token too.

Recipes

Two spreads cover most compositions — both respect reduced motion on their own.

const reduceMotion = useReducedMotion();

<motion.div {...riseIn(reduceMotion)} />          // arrive like a Stack

<AnimatePresence initial={false}>
  <motion.div key={face} {...fadeThroughBlur(reduceMotion)} />
</AnimatePresence>

Distances

Token Value Where
RISE_PX 12 Entrances rise from just below.
BAR_RISE_PX −8 The condensed bar comes from nearer still.
BLUR_PX 1 The blur budget — one breath, everywhere.
PRESS_SCALE 0.96 Press depth for small controls.
PRESS_SCALE_ROW 0.97 Press depth for rows and full-size surfaces.

Guidelines