Auditing a Codebase for Motion Consistency
Part of Motion Design Systems & Tokens in Core CSS Animation Fundamentals.
The problem
A product team wants to introduce a motion system. Before choosing token values, they need to know what they have: how many different durations and curves exist, which components animate what, where reduced motion is handled and where it is missing, and which animations are expensive. Nobody has that picture; the motion grew over years, across several frameworks, some of it in CSS and some in script.
Guessing produces a token scale that fits nothing. An audit produces a scale that maps onto what already ships, plus a prioritised list of fixes.
Root cause analysis: motion hides in three places
Stylesheets contain most declared motion: transition and animation shorthands and longhands, @keyframes rules, and media queries for prefers-reduced-motion. Static extraction with a CSS parser finds them reliably, including values that are never actually used at runtime.
Script contains the rest: element.animate() calls, animation library configurations and inline styles set in handlers. These are hard to find statically because values are often computed.
Runtime is the ground truth. document.getAnimations() after loading a page and exercising its interactions lists every CSS animation, transition and script animation that actually ran, with real durations, easings and targets — as described in inspecting running animations with getAnimations(). It misses code paths nobody exercised, which is why static and runtime inventories complement each other.
Clustering reveals the real scale. Raw values rarely line up: 200ms, .2s, 210ms and 220ms are one intention written four ways. Grouping durations within about 20% of each other and easings with control points within a small tolerance usually collapses dozens of values into five or six clusters, which become candidate token steps.
Quality issues ride along. The same inventory shows animated properties — so layout and paint animations on large elements can be flagged — and, by capturing once with reduced motion emulated, which animations ignore the preference.
Step-by-step resolution
Production code pattern
// audit/static.mjs — extract motion declarations with PostCSS.
import postcss from 'postcss';
import { readFileSync } from 'node:fs';
import { globSync } from 'glob';
const rows = [];
for (const file of globSync('src/**/*.css')) {
const root = postcss.parse(readFileSync(file, 'utf8'), { from: file });
root.walkDecls(/^(transition|animation)(-duration|-timing-function|-delay)?$/, (decl) => {
const inReducedMotion = !!decl.parent?.parent?.params?.includes('prefers-reduced-motion');
const durations = [...decl.value.matchAll(/(\d*\.?\d+)(ms|s)\b/g)]
.map(([, n, u]) => (u === 's' ? parseFloat(n) * 1000 : parseFloat(n)));
const easings = decl.value.match(/cubic-bezier\([^)]*\)|linear\([^)]*\)|\bease(-in|-out|-in-out)?\b|steps\([^)]*\)/g) ?? [];
rows.push({ file, selector: decl.parent.selector, prop: decl.prop, durations, easings, inReducedMotion });
});
}
process.stdout.write(JSON.stringify(rows, null, 2));
// audit/runtime.mjs — capture what actually runs, with and without reduced motion.
import { chromium } from 'playwright';
for (const reducedMotion of ['no-preference', 'reduce']) {
const browser = await chromium.launch();
const page = await browser.newPage({ reducedMotion });
await page.goto('http://localhost:8080/settings');
await page.click('[data-test=open-filters]');
const captured = await page.evaluate(() => document.getAnimations().map((a) => {
const t = a.effect.getComputedTiming();
const kf = a.effect.getKeyframes();
return {
name: a.animationName ?? a.transitionProperty ?? a.id ?? 'script',
duration: t.duration, easing: t.easing, iterations: t.iterations,
properties: [...new Set(kf.flatMap(Object.keys))].filter((k) => !['offset', 'computedOffset', 'easing', 'composite'].includes(k)),
};
}));
console.log(reducedMotion, JSON.stringify(captured));
await browser.close();
}
/* Audit outcome applied: one cluster mapped to one token. */
.filters { transition: translate var(--motion-enter-duration) var(--motion-enter-easing); }
@media (prefers-reduced-motion: reduce) {
.filters { transition: none; }
}
Rendering Impact: none for the audit itself. Its output identifies rendering-tier problems — layout and paint animations — that the fixes then remove.
Recording whether each declaration sits inside a prefers-reduced-motion block in the static pass, and comparing runtime inventories under both settings, gives two independent views of coverage. Static analysis shows where reduced motion was written; runtime shows where it works, including cases where an override loses to a later, more specific rule.
Presenting the audit
The audit’s audience is both design and engineering, so present it visually: a histogram of durations with cluster boundaries drawn on it, a gallery of the distinct easing curves plotted together, and a table of components ranked by the number of issues multiplied by their reuse. The histogram usually makes the case for a token scale on its own — clusters are obvious, and so are the outliers. Keep the raw inventories in the repository so the audit can be re-run after migration, and use the before-and-after counts as the success measure for the Stylelint rollout.
Verification checklist
Constraints and trade-offs
- Runtime capture only sees exercised paths; add interactions for dialogs, errors and empty states.
- CSS-in-JS and inline styles may need framework-specific extraction.
- Clustering tolerances are judgement calls; review cluster boundaries with designers.
- Third-party widgets appear in runtime inventories but are usually outside the team’s control.
- An audit is a snapshot; re-run it periodically or after major releases.
Frequently asked questions
How do I find all animations in a large codebase?
Combine static extraction from stylesheets with runtime capture using document.getAnimations() on key pages and interactions. Each finds cases the other misses.
How do I turn audit results into tokens?
Cluster durations within about 20 percent and easings with near-identical curves, then choose one representative value per cluster as a token step.
How can an audit check reduced-motion support?
Capture running animations with and without reduced motion emulated and compare. Animations that still travel or loop under reduce are gaps.
What should be fixed first?
Shared components and high-traffic pages, and any expensive layout animations or missing reduced-motion branches, because they have the widest impact.
Related
- Motion Design Systems & Tokens — the parent topic
- Building a Duration and Easing Token Scale — turning clusters into a scale
- Manual Motion Accessibility Audit Checklist — the human review that complements automation