Motion Design: Micro-Interactions That Mean Something
Chapter objectives
- Tune the duration, curve and property of every animation
- Choreograph entrances and scroll transitions
- Respect performance and prefers-reduced-motion
A perfect page, but frozen
The Sereno landing is now typeset, themed light and dark, accessible. The client scrolls through it in silence, then says: "It's very beautiful. But it's… motionless. It looks like a poster." Studio Mango's director translates: it's missing motion — that layer of movement that makes an interface feel alive and responsive under the fingers. Chapter 5 set the rule (short, gentle, meaningful); today, you build the complete motion system, with the same rigor as for colors and type.
Because that's the key point: motion is designed in tokens and rules, not in effects added one by one. An interface where every element moves at its own speed with its own curve looks hacked together — exactly like a page with random spacing. A small number of durations, one or two signature curves, usage rules: that's what you're going to deliver.
Movement is a functional language
Before the settings, the why. A well-designed animation fulfills one of three roles. Feedback: confirming that an action was registered — the button that sinks slightly on click says "I heard you". Orientation: showing where an element comes from and where it's going — the panel that slides in from the right indicates it will close toward the right. Continuity: linking two states of an interface so the change isn't a brutal jump — the card that expands smoothly rather than a modal popping out of nowhere.
Any animation that fulfills none of these roles is decorative — noise. For a meditation app, the criterion is even stricter: every movement must soothe, never distract. Ask yourself for each animation: "what does it tell the user?". If the answer is "nothing, but it's pretty", remove it. It's the motion version of the hunt for the generic.
The three settings: duration, curve, property
First decision: the duration, proportional to the scale of the change. Micro-feedbacks (hover, focus, checked box) live between 100 and 200 ms — shorter feels instantaneous, longer feels sluggish. Medium transitions (a card appearing, an accordion unfolding) between 200 and 300 ms. Large movements (side panel, page change) between 300 and 500 ms — beyond that, the user is waiting for the interface, and repeated waiting becomes irritation.
Second decision: the easing curve. The physical rule: what enters the screen decelerates (ease-out — the element arrives fast and settles gently), what exits accelerates (ease-in — it escapes), what moves in place does both (ease-in-out). linear is reserved for continuous rotations like spinners. And to give Sereno a signature, you define a custom cubic-bezier curve, gentle and slightly damped, used everywhere — it's the motion equivalent of the heading typeface.
:root {
/* Motion tokens — the system, not isolated effects */
--duration-fast: 150ms; /* micro-feedback: hover, focus */
--duration-base: 250ms; /* medium transitions: cards, accordions */
--duration-slow: 400ms; /* large movements: panels, pages */
--ease-out-soft: cubic-bezier(0.25, 0.8, 0.35, 1); /* the Sereno signature */
--ease-in-soft: cubic-bezier(0.55, 0, 0.7, 0.4);
}
.btn {
transition:
background-color var(--duration-fast) var(--ease-out-soft),
transform var(--duration-fast) var(--ease-out-soft),
box-shadow var(--duration-fast) var(--ease-out-soft);
}
.btn:hover { transform: translateY(-1px); }
.btn:active { transform: translateY(0) scale(0.98); }--ease-out-soft) and ban improvised easings in your prompts, exactly as you ban hard-coded colors.Performance: animate only transform and opacity
Third decision, the most technical: the animated property. Not all CSS properties are equal. Animating width, height, top or margin forces the browser to recompute the layout of the whole zone at every frame — on an average phone, the animation stutters. Animating transform (translation, scale, rotation) and opacity is handled directly by the graphics processor: smooth at 60 frames per second, even on modest hardware.
The practical translation: an element that "lifts" on hover uses transform: translateY(-2px), never top: -2px. A panel that opens slides with translateX, it doesn't change its width. An entrance combines opacity and a small translation. Add this constraint to all your motion prompts — it's one of those requirements the AI respects perfectly when stated, and forgets half the time otherwise.
will-change: useful occasionally to prepare a heavy animation, it consumes memory if placed everywhere "as a precaution". The rule: only add it if an animation actually stutters, and remove it after the animation.Choreography: ordering the entrances
A page doesn't appear as one block: it tells itself. Choreography means ordering the entrances according to the reading hierarchy: the heading first, the subtitle next, the CTA last — each offset by 50 to 80 ms from the previous one. That offset (the "stagger") is short enough for the whole to feel fluid, long enough for the eye to follow the thread. The three benefit cards appear in a left-to-right cascade, not all at once.
flowchart LR L["Section load"] --> T["Heading: immediate entrance"] T --> ST["Subtitle: 60 ms delay"] ST --> CTA["CTA: 120 ms delay"] CTA --> C1["Card 1"] C1 --> C2["Card 2: plus 70 ms"] C2 --> C3["Card 3: plus 140 ms"]
Scroll reveals, with restraint
The landing page classic: sections revealing themselves gently as you scroll. Well dosed, the effect paces the reading; overdosed, it turns the page into an amusement park. The restraint rules: a short travel distance (12 to 16 px, not 100), a single reveal (the element stays visible once revealed, it doesn't replay the animation on every pass), and one single type of effect for the whole page — fade + slight rise, period. No zoom here, rotation there, bounce elsewhere.
/* CSS: the initial state and the revealed state */
.reveal {
opacity: 0;
transform: translateY(14px);
transition: opacity var(--duration-slow) var(--ease-out-soft),
transform var(--duration-slow) var(--ease-out-soft);
}
.reveal.is-visible { opacity: 1; transform: none; }// JS: IntersectionObserver — reveals only once
const observer = new IntersectionObserver((entries) => {
for (const entry of entries) {
if (entry.isIntersecting) {
entry.target.classList.add('is-visible');
observer.unobserve(entry.target); // only once
}
}
}, { threshold: 0.15 });
document.querySelectorAll('.reveal').forEach((el) => observer.observe(el));Add a complete motion system to the Sereno landing (code attached): - tokens: --duration-fast 150ms, --duration-base 250ms, --duration-slow 400ms, signature curve cubic-bezier(0.25, 0.8, 0.35, 1) - micro-feedbacks: hover and active on buttons, cards and links (transform + shadow, never top/width) - hero choreography: heading, then subtitle (+60ms), then CTA (+120ms), in fade + translateY(14px) - scroll reveals: sections in fade + 14px rise, once only, via IntersectionObserver - CONSTRAINTS: animate only transform and opacity; no animation over 500ms; no bounce or elastic effects - add the @media (prefers-reduced-motion: reduce) block that disables translations and stagger while keeping simple fast fades Give the CSS and the JS separately, commented.
prefers-reduced-motion: non-negotiable
Some users suffer from vestibular disorders: interface movements — parallax, wide slides, zooms — cause them real dizziness and nausea. Others simply prefer a calm interface. All of them can enable "reduce motion" in their system, and your CSS must listen via the prefers-reduced-motion: reduce media query. Reducing doesn't mean breaking everything: you replace translations with simple short fades, and you remove the purely decorative animations. The interface stays alive; it stops moving through space.
@media (prefers-reduced-motion: reduce) {
.reveal {
transform: none; /* no more spatial movement */
transition: opacity 120ms ease; /* a simple short fade */
}
.btn:hover, .btn:active { transform: none; }
}For a meditation app, the irony would be cruel: giving nausea to the user who came looking for calm. Integrate the media query from the very first motion prompt (as above), not as a last-minute patch — and add this point to your delivery checklist, at the same rank as the contrasts.
When not to animate
Motion audit of the Sereno landing (code attached). Draw up an exhaustive table of every animation: - element concerned, trigger, duration, curve, animated properties - its role: feedback, orientation or continuity — write "NONE" if it is purely decorative - compliance: properties limited to transform/opacity? duration under 500 ms? behavior defined under prefers-reduced-motion? Then: 1. list the animations to REMOVE (role NONE) with a one-sentence justification 2. list the technical non-compliances and their fix 3. check that all durations and curves come from the --duration-* and --ease-* tokens, and flag any hard-coded value
Finish with the critical inventory: ask the AI for the list of all the page's animations with their duration, their property and their role (feedback, orientation, continuity). Any line whose role stays empty is a candidate for removal. You'll almost always see the same result: the final page has fewer animations than the intermediate version, but each one is right. Premium motion is like the editing of a good film — you don't notice the cuts, you just feel that everything flows. The client won't say "nice animations"; they'll say "it's soothing", and that's exactly the mission.
Context
"It's motionless," said the client. You have an afternoon to add the motion layer to the Sereno landing: micro-feedbacks, hero choreography, scroll reveals — all smooth on an entry-level phone and respectful of prefers-reduced-motion. The director will be intractable on one point: not a single gratuitous movement.
Instructions
- Define your motion tokens: three durations (150/250/400 ms) and a signature cubic-bezier curve, added to the design system.
- Add the micro-feedbacks to buttons, cards and links — transform and opacity only, 150 ms transitions.
- Choreograph the hero: heading, subtitle, CTA in cascade with a 60 ms stagger, in fade + 14px rise.
- Set up the scroll reveals with IntersectionObserver: a single type of effect, once per element.
- Add the prefers-reduced-motion block that replaces any movement with a short fade, and test it by enabling the option in your system.
- Request the critical inventory: a list of every animation with its role (feedback, orientation, continuity) — remove any line without a role.
In summary
- Motion is designed as a system: duration tokens, a signature curve, usage rules — not isolated effects.
- Every animation fulfills a role: feedback, orientation or continuity — otherwise it's noise to remove.
- Durations: 100-200 ms for micro-feedbacks, 200-300 ms for transitions, 300-500 ms for large movements.
- Ease-out for what enters, ease-in for what exits, ease-in-out for what moves; linear reserved for continuous rotations.
- You animate only transform and opacity: other properties make modest phones stutter.
- Choreography follows the reading hierarchy with a 50-80 ms stagger; on scroll: one effect, once, 12-16 px.
- prefers-reduced-motion is non-negotiable: replace movements with short fades, from the very first prompt.
Quiz — check your understanding
1. What are the three legitimate roles of an interface animation?
2. What duration suits a hover micro-feedback?
3. Why animate only transform and opacity?
4. Which easing curve for an element entering the screen?
5. What must the prefers-reduced-motion: reduce block do?
6. What is a "stagger" in an entrance choreography?