Files
training/skills/hallmark/references/motion.md
2026-07-28 17:12:27 +08:00

4.2 KiB
Raw Blame History

Motion

Most AI-generated motion is scattered — hover lifts on every card, fade-in on every scroll, bouncing icons. Quiet it. One orchestrated moment beats ten small ones.

For per-interaction recipes (button press, focus, modal, toast, optimistic update, command palette, drag handle, etc.), see microinteractions.md. This file is the language of motion; that file is the vocabulary.

Principles

  • Animate only transform and opacity. These are GPU-composited; they don't trigger layout. Anything else is a performance bug waiting.
  • Duration is three buckets. Micro (100150ms), minor (200300ms), major (300500ms). Exits are ~75% of the enter.
  • Easing is exponential ease-out. Elements coming in slow down into place. Elements leaving accelerate away.
  • Motion serves perception. If you can't explain what a transition communicates, cut it.
  • Reduced motion is non-optional. @media (prefers-reduced-motion: reduce) collapses all spatial motion to opacity crossfade.

Easings

Use these three. Name them as tokens.

:root {
  --ease-out: cubic-bezier(0.16, 1, 0.3, 1);        /* elements entering */
  --ease-in:  cubic-bezier(0.7,  0, 0.84, 0);       /* elements leaving  */
  --ease-in-out: cubic-bezier(0.65, 0, 0.35, 1);    /* state toggles     */
}

ease, ease-in-out (default), cubic-bezier(0.25, 0.1, 0.25, 1) — these are the browser defaults and they read as uncrafted.

Durations

:root {
  --dur-micro: 120ms;   /* button press, toggle tick, color shift  */
  --dur-short: 220ms;   /* hover lift, tooltip, menu open          */
  --dur-long:  420ms;   /* modal, drawer, accordion, page reveal   */
}

Exits use roughly 75% of the enter:

.menu.is-open  { transition: transform var(--dur-short) var(--ease-out); }
.menu.is-close { transition: transform calc(var(--dur-short) * 0.75) var(--ease-in); }

Page-load orchestration

One sequence on page load. Stagger by DOM index using a CSS custom property, not by JS.

<section style="--i: 0"></section>
<section style="--i: 1"></section>
<section style="--i: 2"></section>
.reveal {
  opacity: 0;
  transform: translateY(8px);
  animation: reveal var(--dur-long) var(--ease-out) forwards;
  animation-delay: calc(var(--i, 0) * 60ms);
}
@keyframes reveal {
  to { opacity: 1; transform: none; }
}

Cap total stagger at ~500ms. Beyond that the page feels slow to settle.

Scroll-linked motion

  • Use IntersectionObserver, never scroll event listeners.
  • Use it only for reveal once effects. No parallax. No scroll-scrubbed animations unless there is a specific reason.
  • Every scroll-triggered motion must have a reduced-motion fallback.

State transitions

  • Button hover / active: micro duration, --ease-out, transform: translateY(-1px) on hover, translateY(0) on active. Never a box-shadow transition on hover on a dark background (glow effect).
  • Menu / tooltip / dropdown: short duration, --ease-out on open, --ease-in on close. Use the popover API or inert to manage focus.
  • Modal: long duration, scale-in (0.96 → 1) + opacity crossfade. Backdrop fades in at the same duration.
  • Accordion: animate grid-template-rows: 0frgrid-template-rows: 1fr (not height). With --ease-in-out.

Reduced motion

@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    animation-duration: 150ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 150ms !important;
  }
  .reveal { animation: reveal-reduced 150ms linear forwards; }
  @keyframes reveal-reduced { to { opacity: 1; transform: none; } }
}

Functional motion (progress bars, loading spinners, skeletons) still runs — just slower.

Bans

  • ease (browser default, mediocre).
  • linear on anything except progress bars and ticking loaders.
  • Bounce / elastic / overshoot on UI elements. Dated; signals "template".
  • Animating width, height, top, left, margin, padding.
  • will-change set preemptively across a whole class. Only on the element, only while it's animating.
  • Parallax.
  • Custom cursors.
  • Scroll-driven animations without a reduced-motion fallback.
  • Infinite loops (other than functional loaders) — they pull the eye and never let go.