Transitions — Motion Artist
"CSS motion library"
When to use
- A UI works but feels dead or abrupt — "make it feel alive", "everything just pops in".
- Motion exists but is chaotic — "every component animates differently, unify it".
- Specific effects are requested — "animate this modal / dropdown / list / page load".
- Accessibility review flagged motion — "we need prefers-reduced-motion support".
Workflow
- Audit what changes. List every state change in the UI — appear, disappear, reorder,
attention. Motion attaches to changes, not to components.
- Define duration tokens.
--motion-fast: 120ms for micro-feedback (hover, press),
--motion-base: 200ms for most transitions, --motion-slow: 320ms for entrances and large
surfaces. Exits run roughly 20% faster than their enters.
- Define easing tokens. Standard
cubic-bezier(0.2, 0, 0, 1) for most moves; decelerate
cubic-bezier(0, 0, 0, 1) for entrances; spring cubic-bezier(0.34, 1.56, 0.64, 1) reserved
for playful emphasis — at most once per view.
- Build the pattern set. Enter: fade-rise 8px, scale-in 0.96→1. Exit: fade-fall, faster.
Emphasis: one pulse, or one shake for errors — never looping. Only
transform and opacity
ever animate; layout properties are off-limits.
- Set stagger rules. Lists stagger 30–40ms per item, total capped at 400ms; past ten items,
stagger the first eight and land the rest together.
- Write the reduced-motion fallback. Under
prefers-reduced-motion: reduce, collapse
animations to fast opacity fades — never delete feedback entirely.
- Emit the library.
motion.css (tokens + patterns) and a trigger snippet
(IntersectionObserver scroll-ins, class-toggle helpers), with a one-line usage note per
pattern: what it is for, what it must never be used for.
Output format
/* motion.css — tokens */
:root {
--motion-fast: 120ms;
--motion-base: 200ms;
--motion-slow: 320ms;
--ease-standard: cubic-bezier(0.2, 0, 0, 1);
--ease-decelerate: cubic-bezier(0, 0, 0, 1);
--ease-spring: cubic-bezier(0.34, 1.56, 0.64, 1);
}
/* enter — use for: content appearing; never for: hover feedback */
.enter-rise { animation: rise var(--motion-slow) var(--ease-decelerate) both; }
@keyframes rise { from { opacity: 0; transform: translateY(8px); } }
/* stagger — set style="--i: n" per item; cap the cascade at 400ms */
.stagger > * { animation-delay: calc(var(--i, 0) * 35ms); }
/* reduced motion — feedback stays, movement goes */
@media (prefers-reduced-motion: reduce) {
*, *::before, *::after {
animation-duration: 1ms !important;
transition-duration: 80ms !important;
}
}
// scroll-in trigger — pairs with .enter-rise via [data-animate]
const io = new IntersectionObserver(
(entries) => entries.forEach((e) => e.isIntersecting && e.target.classList.add('enter-rise')),
{ threshold: 0.15 }
);
document.querySelectorAll('[data-animate]').forEach((el) => io.observe(el));
Quality bar
Example
Invocation: "My settings modal and dropdown feel abrupt, and list items just pop in."
Produces: motion.css with the token block; modal enter at 320ms decelerate (scale-in) with a
160ms exit; dropdown at 200ms standard with transform-origin: top; a 35ms stagger on the list
capped at 400ms; the IntersectionObserver trigger; and a reduced-motion block that converts every
pattern to a fast opacity fade.
1---2name: transitions3description: Builds a complete CSS motion system — duration and easing tokens (fast 120ms micro-feedback, base 200ms, slow 320ms entrances; standard, decelerate and spring curves), enter/exit/emphasis patterns, stagger rules, and prefers-reduced-motion fallbacks — delivered as a copy-paste CSS/JS snippet library. Use when the user says "add animations", "make this feel smooth", "the transitions are janky", or "animate this modal / dropdown / list without making it feel like a template".4---56# Transitions — Motion Artist78> "CSS motion library"910## When to use1112- A UI works but feels dead or abrupt — "make it feel alive", "everything just pops in".13- Motion exists but is chaotic — "every component animates differently, unify it".14- Specific effects are requested — "animate this modal / dropdown / list / page load".15- Accessibility review flagged motion — "we need prefers-reduced-motion support".1617## Workflow18191. **Audit what changes.** List every state change in the UI — appear, disappear, reorder,20 attention. Motion attaches to changes, not to components.212. **Define duration tokens.** `--motion-fast: 120ms` for micro-feedback (hover, press),22 `--motion-base: 200ms` for most transitions, `--motion-slow: 320ms` for entrances and large23 surfaces. Exits run roughly 20% faster than their enters.243. **Define easing tokens.** Standard `cubic-bezier(0.2, 0, 0, 1)` for most moves; decelerate25 `cubic-bezier(0, 0, 0, 1)` for entrances; spring `cubic-bezier(0.34, 1.56, 0.64, 1)` reserved26 for playful emphasis — at most once per view.274. **Build the pattern set.** Enter: fade-rise 8px, scale-in 0.96→1. Exit: fade-fall, faster.28 Emphasis: one pulse, or one shake for errors — never looping. Only `transform` and `opacity`29 ever animate; layout properties are off-limits.305. **Set stagger rules.** Lists stagger 30–40ms per item, total capped at 400ms; past ten items,31 stagger the first eight and land the rest together.326. **Write the reduced-motion fallback.** Under `prefers-reduced-motion: reduce`, collapse33 animations to fast opacity fades — never delete feedback entirely.347. **Emit the library.** `motion.css` (tokens + patterns) and a trigger snippet35 (IntersectionObserver scroll-ins, class-toggle helpers), with a one-line usage note per36 pattern: what it is for, what it must never be used for.3738## Output format3940```css41/* motion.css — tokens */42:root {43 --motion-fast: 120ms;44 --motion-base: 200ms;45 --motion-slow: 320ms;46 --ease-standard: cubic-bezier(0.2, 0, 0, 1);47 --ease-decelerate: cubic-bezier(0, 0, 0, 1);48 --ease-spring: cubic-bezier(0.34, 1.56, 0.64, 1);49}50/* enter — use for: content appearing; never for: hover feedback */51.enter-rise { animation: rise var(--motion-slow) var(--ease-decelerate) both; }52@keyframes rise { from { opacity: 0; transform: translateY(8px); } }53/* stagger — set style="--i: n" per item; cap the cascade at 400ms */54.stagger > * { animation-delay: calc(var(--i, 0) * 35ms); }55/* reduced motion — feedback stays, movement goes */56@media (prefers-reduced-motion: reduce) {57 *, *::before, *::after {58 animation-duration: 1ms !important;59 transition-duration: 80ms !important;60 }61}62```6364```js65// scroll-in trigger — pairs with .enter-rise via [data-animate]66const io = new IntersectionObserver(67 (entries) => entries.forEach((e) => e.isIntersecting && e.target.classList.add('enter-rise')),68 { threshold: 0.15 }69);70document.querySelectorAll('[data-animate]').forEach((el) => io.observe(el));71```7273## Quality bar7475- [ ] Every duration and easing is a token — zero inline magic numbers76- [ ] Exits faster than enters; nothing exceeds 400ms except a declared hero moment77- [ ] Only transform and opacity animate — no layout properties, no `transition: all`78- [ ] prefers-reduced-motion fallback present and verified by toggling the OS setting79- [ ] Spring easing appears at most once per view80- [ ] Staggers capped — no two-second cascades on long lists8182## Example8384**Invocation:** "My settings modal and dropdown feel abrupt, and list items just pop in."8586**Produces:** `motion.css` with the token block; modal enter at 320ms decelerate (scale-in) with a87160ms exit; dropdown at 200ms standard with `transform-origin: top`; a 35ms stagger on the list88capped at 400ms; the IntersectionObserver trigger; and a reduced-motion block that converts every89pattern to a fast opacity fade.