Tutorial

Motion and microinteractions.

Engineers get motion wrong in both directions. Half ship interfaces where every state change teleports: the modal is absent, then present, and the user reconstructs what happened. The other half discover CSS animation and stagger-fade the entire landing page. Both fail the same test. Motion is information. It explains what changed, what caused it, and where a thing went. A transition that explains nothing is noise, and noise on every interaction is worse than silence.

The short version
  • Motion explains cause, state, and place. If you cannot say what a transition explains, delete it.
  • Live in the 150 to 300ms band. Under 100ms reads as a glitch; over 400ms reads as a wait.
  • Ease out on entrances, ease in on exits. Linear easing is for opacity and spinners only.
  • Transition transform and opacity. Leave width, height, and top to layout.
  • Ship prefers-reduced-motion support before shipping any animation.

Motion is information, or it is noise

Every animation in a shipped interface should answer one of three questions. What changed? A toggle sliding tells you the state flipped; a toggle repainting makes you check. What caused it? A menu growing out of the button that opened it binds effect to cause. Where did it go? An item shrinking toward the cart icon tells you where to find it later. That is the whole taxonomy. Decorative motion, the kind that answers no question, spends the user's attention and gives nothing back.

This gives you a one-sentence taste test before writing any keyframe: state what the animation explains. “The dropdown descends from the field that owns it” passes. “The cards feel more dynamic” fails. No sentence, no animation.

The duration band: 150 to 300ms

Nearly every interface transition belongs between 150 and 300ms. Below about 100ms, motion stops registering as motion; the element appears to flicker into place, which is worse than appearing instantly. Above 400ms, the interface starts making the user wait for choreography, and by the fifth encounter that wait is resentment. The band is narrow, and within it, size decides.

What is movingDuration
Hover and press feedback100 to 150ms. Feedback must feel attached to the pointer.
Toggles, tabs, small state flips150 to 200ms.
Dropdowns, toasts, tooltips200 to 250ms entering; exits about a third faster.
Modals, drawers, panels250 to 300ms. Big elements need a few more frames to read.
Full page transitions300 to 400ms, and rarely. This is the ceiling, not the default.

Two asymmetries worth internalizing. Exits run faster than entrances, because a dismissed element is already forgotten; nobody watches a closing menu. And repeated interactions run faster than rare ones: a tooltip the user triggers forty times a day earns 100ms, a first-run welcome panel can take 300. Encode the band as tokens so every duration in the codebase is a decision made once:

:root {
  --ease-out: cubic-bezier(0.2, 0, 0, 1);
  --ease-in: cubic-bezier(0.5, 0, 1, 1);
  --dur-fast: 150ms;   /* feedback, exits */
  --dur-base: 200ms;   /* most state changes */
  --dur-slow: 300ms;   /* modals, panels */
}

Easing: curves that feel physical

Nothing in the physical world moves at constant speed. Objects accelerate from rest and decelerate into place, and a lifetime of watching them is why linear movement reads as mechanical, like a garage door. The two curves that matter map directly onto that intuition:

  • Ease-out for entrances. The element arrives fast and decelerates into its final position, so the interface feels responsive at the start and settled at the end. This is the default for almost everything.
  • Ease-in for exits. The element accelerates away, like something tossed offscreen. Pair it with the shorter exit duration.
LINEAR Constant speed. Reads as mechanical. EASE-OUT · cubic-bezier(0.2, 0, 0, 1) Fast arrival, gentle landing. Reads as physical.
Progress over time. The ease-out curve covers most of its distance early, so the interface feels immediate, then spends its last frames settling.

The browser's built-in ease-out is serviceable but timid. The curve above, cubic-bezier(0.2, 0, 0, 1), is the shape used across Material Design and most polished products: a sharper initial burst, a longer settle. Drop it into the tokens once and stop deciding per element. The one place linear easing belongs: pure opacity fades and looping spinners, where there is no position for physics to act on.

Transition transform and opacity, nothing else

Browsers animate transform and opacity on the compositor thread: no layout, no repaint, smooth at 60fps even while your JS thread is busy hydrating. Animating height, width, top, left, or margin forces the browser to recompute layout on every frame, and it stutters exactly when the page is under load, which is exactly when your users are watching.

/* Janky: layout runs every frame */
.toast { transition: bottom 200ms; }

/* Smooth: compositor only */
.toast {
  opacity: 0;
  transform: translateY(8px);
  transition: opacity var(--dur-base) linear,
              transform var(--dur-base) var(--ease-out);
}
.toast.open { opacity: 1; transform: translateY(0); }

The taste consequence is bigger than the performance one: constraining yourself to transform and opacity produces better motion. Slide, scale, fade, and combinations of the three cover nearly every transition an interface needs. The animations that violate the rule, accordion heights, morphing widths, are usually the over-designed ones. When you genuinely need an animated height, measure it in JS and animate with the FLIP technique, or reach for grid-template-rows: 0fr to 1fr, which modern browsers transition cleanly.

Four microinteractions worth building

Hover. The job is to say “this is interactive” before the click. Small, fast, cheap: a 1px lift, a background shift, 150ms out. The press state matters more than most engineers think; snapping back near-instantly on :active is what makes a button feel solid instead of spongy.

.btn {
  transition: transform 150ms var(--ease-out),
              background 150ms var(--ease-out);
}
.btn:hover  { transform: translateY(-1px); }
.btn:active { transform: translateY(0); transition-duration: 50ms; }

Focus. The exception that proves every rule so far: focus indication should not animate. A keyboard user tabbing through a form needs the ring the same frame the element gains focus; a 200ms fade makes fast tabbing feel laggy and can strand the eye. Use :focus-visible, keep it instant, keep it loud:

.btn:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
  /* no transition on purpose */
}

Loading. The taste move is a delay before the indicator, not a fancier indicator. A response that returns in 300ms should never flash a spinner; the flash reads as slowness the request did not have. Hold the indicator back about 200ms, and if it does appear, keep it up for a minimum beat so it cannot strobe:

const t = setTimeout(() => btn.classList.add("loading"), 200);
try {
  await save();
} finally {
  clearTimeout(t);
  btn.classList.remove("loading");
}

Success. A saved state deserves confirmation where the action happened: the button's label swaps to a check, holds for 1.5 to 2 seconds, and returns. That is the entire pattern. Confetti, full-screen overlays, and toast-plus-banner-plus-checkmark stacks are three answers to a question the user asked once.

Respect prefers-reduced-motion

Vestibular disorders make sliding and zooming interfaces physically unpleasant for a meaningful slice of users, and every major OS ships a reduce-motion setting that your CSS can read. Honoring it is not optional polish; it is the difference between an interface and a liability. The craft point: reduce means reduce movement, not remove feedback. Keep the opacity fades, drop the travel:

@media (prefers-reduced-motion: reduce) {
  .toast {
    transform: none;
    transition: opacity var(--dur-base) linear;
  }
}

For JS-driven animation, read the same flag and branch on duration:

const reduce =
  matchMedia("(prefers-reduced-motion: reduce)").matches;
el.animate(keyframes, {
  duration: reduce ? 0 : 200,
  easing: "cubic-bezier(0.2, 0, 0, 1)",
});

Build this in from the first transition. Retrofitting reduce-motion across a codebase that never planned for it is a slog; supporting it from a tokens file is two rules.

The restraint rule: animate one thing well

Here is the discipline that separates products that feel expensive from portfolios that feel decorated: on any state change, animate one element, and make it the element the user caused. The menu opens; the menu animates. Not the menu plus a nav underline plus a background dim that fades at a different rate. When several things must move at once, they move as one choreographed unit on one clock, which is why good modals fade their overlay and slide their panel with the same duration and curve.

The same rule kills the scroll-triggered fade-in epidemic. Staggering every section of a page into view tells the user that everything is equally special, which is another way of saying nothing is. If one number or one demo deserves an entrance, give it one, and let the rest of the page hold still like the confident thing it is. Restraint is legible. Users cannot name the easing curve, but they can feel the difference between an interface where motion means something and one where it is texture.

A concrete audit for the product you already have: open it, trigger every interaction, and write the one-sentence explanation for each animation you find. Delete the ones without a sentence, retime the survivors into the band, and unify their curves through the tokens. That pass takes an afternoon and reads as a redesign.

Keep going

Motion is the last layer; it amplifies whatever structure sits beneath it. This series builds that structure: deconstructing great interfaces teaches you to see what quality is made of, typography for engineers and spacing, color, and hierarchy cover the static craft, and presentation design applies all of it to the deck you present next. The design taste resources page collects the references worth your evenings, and the standard we hold this work to lives in the taste rubric.

Learn this beside the people building it.

Membership is free. Masterclasses from industry leaders, hackathons where you finish something the same day, and mentor circles matched to what you want to learn. For engineers and creatives alike, across film, design, image, sound, and story.