Design Engineering
Quality & Polish

Animation & Motion

Use animation intentionally — CSS transitions, keyframe animations, Framer Motion, gesture handling, page transitions, and reduced motion support.

Animation is where design engineers add unique value. A prototype has no motion curves, no hover states, no gesture feedback — but a shipped product needs all of them. Motion communicates hierarchy, confirms actions, and makes the interface feel physical rather than mechanical.

CSS Transitions vs Keyframes

The two CSS animation mechanisms serve different purposes and behave differently when interrupted:

MechanismInterruptible?Best for
CSS transitionsYes — smoothly retarget to the latest stateInteractive elements: hover, open/close, toggle, focus
CSS keyframe animationsNo — run on a fixed timeline once startedDecorative sequences: mount animations, loading indicators, attention-getters
Transition — interruptible, safe for interaction
.element {
  transition: transform 200ms ease, opacity 200ms ease;
}
Keyframe — not interruptible, best for one-shot sequences
@keyframes fade-in {
  from { opacity: 0; transform: translateY(8px); }
  to   { opacity: 1; transform: translateY(0); }
}
.element {
  animation: fade-in 400ms ease-out;
}

Users change their intent mid-interaction — menus open then close, dropdowns toggle then toggle back. Non-interruptible animations make the interface feel sluggish. Use transitions for anything the user interacts with.

Timing & Easing Tokens

Define motion durations and easing curves as design tokens:

Motion tokens in @theme
@theme inline {
  --duration-instant:  50ms;
  --duration-fast:     100ms;
  --duration-moderate: 200ms;
  --duration-slow:     400ms;
  --ease-out:   cubic-bezier(0.0, 0.0, 0.2, 1);
  --ease-in:    cubic-bezier(0.4, 0.0, 1, 1);
  --ease-inout: cubic-bezier(0.4, 0.0, 0.2, 1);
}
DurationTypical use
50msFeedback: hover states, focus rings
100msSmall element entrances
200msMost UI transitions: modals, drawers, dropdowns
400msLarge-area transitions, page changes

Framer Motion

When CSS transitions are not expressive enough — gesture-driven animations, shared layout animations, or orchestrated sequences — use Framer Motion.

Key Framer Motion patterns for design engineers:

PatternAPIWhen to use
Layout animationmotion.div layoutItems that reorder, appear, or disappear (lists, accordions, grids)
Presence animation<AnimatePresence>Elements that mount/unmount in UI (modals, toasts, notifications)
Drag gesturesdrag, onDragEnd, dragConstraintsSwipeable cards, draggable lists, sortable grids
Scroll-drivenuseScroll, useTransform, whileInViewScroll reveals, parallax, progress indicators
Variantsvariants={{ hidden: ..., visible: ... }}Orchestrated child animations — stagger children with staggerChildren

Gesture Handling

Touch and pointer gestures need libraries — do not hand-roll drag detection:

Use the drag prop from Framer Motion rather than raw pointer events. It handles velocity calculations, momentum, constraint boundaries, and accessibility fallbacks automatically. The same applies to use-gesture or @use-gesture/react for non-Framer projects.

Page Transitions

View Transitions API enables smooth page transitions without losing scroll position. Supported in Chromium and Safari.

For crossfade transitions, apply CSS:

::view-transition-old(root) {
  animation: fade-out 200ms ease;
}
::view-transition-new(root) {
  animation: fade-in 200ms ease;
}

Reduced Motion

Always respect the user's motion preference. This is not optional:

Respect prefers-reduced-motion
@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    animation-duration: 0.01ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.01ms !important;
  }
}

In Tailwind v4, use the motion-safe: and motion-reduce: variants:

<div
  className="motion-safe:animate-fade-in motion-reduce:opacity-100"
>
  {children}
</div>

Do not disable all motion completely for users who prefer reduced motion. Replace animations with simpler alternatives — fade instead of slide, instant reveal instead of staggered entrance — rather than eliminating motion entirely. Some motion is functional (loading indicators, scroll position, state transitions) and removing it impairs usability.

Optical Alignment

Geometric centering is not always visually correct. Icons with pointed shapes (triangles, chevrons, arrows) often need a slight offset to look centred next to text or inside a button.

Optically align icon inside a button
<button className="inline-flex items-center gap-2 px-4 py-2">
  <PlayIcon className="size-4 mr-[1px]" aria-hidden="true" />
  Play
</button>

The same principle applies to button padding: the side with the icon needs slightly less padding than the text-only side because the icon's visual weight sits differently from text. When in doubt, adjust by 1-2px and check with your eyes rather than trusting the math.

On this page