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
- Reach for a token before writing a number; if a surface needs a new number, it probably wants a new name here first.
- Fades ease out, travel is linear — never the other way around.
- Arrive, don’t slide: entrances rise
RISE_PX, not a screen-width. - One breath of blur (
BLUR_PX) is the whole budget; more reads as glass. - Every recipe collapses under reduced motion — pass
useReducedMotion()through, never guard by hand.