Motion
One animation language: five durations and three easings for CSS, four springs for JS. Every value ships as a token — durations collapse to 0ms under prefers-reduced-motion or the forced data-reduced-motion attribute, and JS springs read useReducedMotion.
Durations
CSS transitions read the duration vars directly, so the reduced-motion collapse needs no component code.
--ch-motion-duration-instant · 80ms
State flips — checkbox marks, toggles
--ch-motion-duration-fast · 160ms
Hover/focus color shifts, chips
--ch-motion-duration-base · 240ms
Standard transitions — accordions, tabs
--ch-motion-duration-slow · 420ms
Panels, drawers, page-level moves
--ch-motion-duration-glacial · 800ms
Ambient/ceremonial motion only
<div className="transition-colors duration-[var(--ch-motion-duration-fast)]" />--ch-motion-duration-instant--ch-motion-duration-fast--ch-motion-duration-base--ch-motion-duration-slow--ch-motion-duration-glacial
Easings
Cubic-bezier curves; tide is the signature draw.
cubic-bezier(0.16, 1, 0.3, 1)
Default — decisive arrivals
cubic-bezier(0.65, 0, 0.35, 1)
Symmetric moves within a view
cubic-bezier(0.32, 0.72, 0, 1)
Signature — long draws, drawers, harbor swells
<div className="transition-transform ease-[var(--ch-motion-ease-tide)] duration-[var(--ch-motion-duration-slow)]" />--ch-motion-ease-out--ch-motion-ease-in-out--ch-motion-ease-tide
Springs
The beUI-derived house springs, exported as @coldharbor/tokens/motion. JS animation must import these — never inline stiffness/damping.
stiffness 600 · damping 30
Press feedback on controls
stiffness 380 · damping 34
Shared-layout pills, sliding indicators
stiffness 260 · damping 26
Expanding surfaces — accordions, sheets
stiffness 500 · damping 18
Playful arrivals — badges, confirmations
import { springs } from "@coldharbor/tokens/motion";
<motion.div animate={{ x }} transition={springs.glide} />springs.presssprings.glidesprings.unfoldsprings.bounce
Reduced motion
Left: live tokens. Right: the same demos inside a data-reduced-motion="reduce" wrapper — the duration vars collapse to 0ms in CSS, and JS springs drop to duration 0 via useReducedMotion / the forced app setting.
base duration + glide spring at full value.
Same components, zero movement — position still updates instantly, nothing is lost but the choreography.
/* CSS side — free: the vars collapse to 0ms. */
@media (prefers-reduced-motion: reduce) { … }
[data-reduced-motion="reduce"] { --ch-motion-duration-base: 0ms; … }
/* JS side */
const reduce = useReducedMotion();
<motion.div transition={reduce ? { duration: 0 } : springs.glide} />--ch-motion-duration-instant--ch-motion-duration-fast--ch-motion-duration-base--ch-motion-duration-slow--ch-motion-duration-glacial