Skip to content
ssntl/ui

Motion

Motion explains a change; it never decorates. The page is complete without it.

Before animating

  1. 01

    How often does it happen?

    Repeated actions (typing, toggling, hovering) feel nearly instant or don't animate. Expressive motion is for rare moments.

  2. 02

    What is it for?

    Explain space, confirm input, show state or soften a change. If it does none, don't animate.

  3. 03

    Which physics?

    --ease-out to enter and exit, --ease-in-out to move what is already on screen, linear only for progress and springs for gestures.

  4. 04

    How does it look with reduced motion?

    Design it at the same time: keep opacity, color and state changes; remove travel, scale, parallax and bounce.

Tokens

TokenValueUse
--ease-outcubic-bezier(.22, 1, .36, 1)Entrances, exits, press and hover: responds at once, then settles.
--ease-in-outcubic-bezier(.65, 0, .35, 1)Something already on screen moving to a new place.
--ease-linearlinearProgress only.
--ease-expressivecubic-bezier(.32, .72, 0, 1)Demos, video and large spatial changes.
--duration-fast150msPress and hover (100–160ms).
--duration-pop180msTooltips and popovers (125–200ms).
--duration-menu220msDropdowns and selects (150–250ms).
--duration-panel320msModals, drawers and sheets (200–500ms).
--duration-slow700msMarketing demos and video only.
--es-lift8pxTravel of an interface entrance. Video uses 24px.
--es-press.96Scale on press for every control.

Timing

Under 300ms by default in the interface. Longer motion is for demos, deliberate gestures and large spatial changes.

InteractionTokenRangeFeel
Press and hover--duration-fast100–160msImmediate and physical
Tooltip, popover--duration-pop125–200msQuick, born from its origin
Dropdown, select--duration-menu150–250msResponsive, no waiting
Modal, drawer, sheet--duration-panel200–500msJust enough to explain space
Marketing demo, video--duration-slowFlexibleClarity over speed

Press

Confirm input before the action finishes: scale to 0.96 in 150ms. It is what Button does.

tsx
<button className="transition-[scale,background-color] duration-(--duration-fast) ease-out active:scale-(--es-press) motion-reduce:active:scale-100">
  Continue
</button>

Entrance

One surface at a time rises 8px and fades in, finishing before it becomes the focus. Lists stagger 30–50ms outward from what you touched.

css
.reveal {
  animation: reveal 700ms var(--ease-out) both;
  animation-delay: calc(var(--d, 0) * 100ms); /* staggers 100ms per piece */
}

@keyframes reveal {
  from { opacity: 0; translate: 0 var(--es-lift); }
}

/* Reduced motion: the content is simply there. */
@media (prefers-reduced-motion: reduce) {
  .reveal { animation: none; }
}

Icons with meaning

tsx
// The icon acts out its meaning: the arrow turns toward its destination.
// Pointer only, and never with reduced motion.
<ArrowRight className="transition-[rotate] duration-(--duration-fast) ease-out group-hover:-rotate-45 motion-reduce:group-hover:rotate-0" />

Continuity and content swaps

  • When a surface changes size or place it stays visible: its shape moves first, then its label arrives.
  • In a content swap, the old content leaves faster than the new arrives (about 60% of the time) and travels a few pixels at most.
  • A menu, popover or sheet grows from the control that opened it (transform-origin) and leaves the way it came.

Gestures

tsx
import { motion, useReducedMotion } from "motion/react"

// Gestures use springs: critically damped (no bounce, ~0.4s).
// They animate from the current value, so a gesture can be grabbed and reversed.
const reduce = useReducedMotion()

<motion.div
  drag="x"
  dragConstraints={{ left: 0, right: 0 }}
  transition={reduce ? { duration: 0 } : { type: "spring", bounce: 0, duration: 0.4 }}
/>

Reduced motion

A designed state, not an absence: keep opacity, color and instant changes; remove travel, scale, parallax and bounce. No state change lives only in an animation.

ts
const hidden = { opacity: 0, translate: reduce ? "0 0" : "0 var(--es-lift)" }
const visible = { opacity: 1, translate: "0 0" }

Never

  • Marquees that scroll on their own.
  • Typewriter effects.
  • Pulsing status dots.
  • Parallax.
  • Revealing every section on scroll.
  • Bounce on things nobody threw.
  • Animating anything but transform and opacity.