Rule handbook metadata
- Rule
DET.MOTION.PREFERS_REDUCED· lanedeterministic- Page status
- current
- page_version
49a23233fa7dbc7d7d980dd0891b238b19c782633c1e748896a0c6aeeac33ba5- generated_at
- 2026-05-25T16:30:00.000Z
- registry_fingerprint
2ce40848effce579d3e4879f6ca85535183a14db201870a6c007da424624550c
How this rule is fixed
Pilot deterministic fixer: handbook_after · harness mode standalone · verify expect_rule_clean.
- Harness:
apply-harness-fixture-remediation.pycopies the After example HTML from this handbook page onto the defect fixture, theninvoke-det-ruleset-remediation-verify.shre-audits and expects zero findings for this rule. - Production sites:
run-website-ux-remediation-loop.shrunshandbook_html_patch(HTML patches inlib/ux-deterministic-fixers/fixers/patches/) before invoking the Cursor agent when the quality gate still fails.
Detection module: docs/design/ux-audit/deterministic-design-rules.md#det-motion-prefers_reduced. Scroll down for Before / After examples and Evidence and remediation steps.
Purpose
Visitors who enable Reduce motion in their OS expect pages to hold still unless motion communicates essential status. Kitchen Sink ships decorative motion in Ksc (css/forge-theme.css: .pulse, .pulse-amber, .breathe-link, .breathe-static, .ks-tilt-wrap, .forge-card hover transitions), ambient layers (css/forge-ambient.css, css/ks-living-background.css), and script fly-ins (css/script-assembly.css). Theme CSS already gates some surfaces — .forge-card transitions, .ks-tilt-inner / .ks-parallax-inner, theme-dropdown aurora, ambient shimmer, living-background drift, and .ks-sa-root blanket overrides — but .pulse / .breathe-* still need explicit @media (prefers-reduced-motion: reduce) blocks when used on consumer pages. This rule verifies that every visible surface respects reduced motion at audit time.
The check module design-rules/deterministic/generated/det-motion-prefers-reduced.check.js emulates prefers-reduced-motion: reduce in Playwright, then scans visible DOM for:
- Active CSS animations with duration above ~0.02s (20ms) that are not essential loading/progress indicators.
- Active CSS transitions with duration above ~0.02s on non-essential elements.
- Autoplay video (
<video autoplay>) still visible in the viewport. - SVG SMIL (
<animate>,<animateTransform>,<set>) that CSS media queries cannot disable.
Essential motion (allowed to continue): elements with aria-busy="true", role="progressbar" or role="status", or class/animation names matching spinner, loading, skeleton, or progress.
Plan: Inventory decorative animation, hover transitions, ambient loops, autoplay video, and inline SMIL on first-screen URLs. Do: Add or extend @media (prefers-reduced-motion: reduce) blocks in theme CSS and page-local styles; pause decorative video; replace SMIL with static SVG or script guards. Check: Run the auditor metrics phase or auditor-tests/invoke-det-ruleset-harness.sh --only-rule DET.MOTION.PREFERS_REDUCED. Adjust: When motion still feels noisy but passes this gate, pair with AI.MOTION.INTENTIONALITY; when slow ambient loops compete with copy, review AI.AMBIENT.READABILITY_CONFLICT.
Passing signals
- Stylesheets include
@media (prefers-reduced-motion: reduce)rules that disable decorative animation (animation: noneor near-zero duration) and transitions (transition: noneor ≤ 0.02s) — see KS patterns inforge-theme.css(.forge-card,.ks-tilt-inner),forge-ambient.css(.fa-css-shimmer),ks-living-background.css(.ks-living-drift-layer), andscript-assembly.css(.ks-sa-rootblanket override). - Always-on pulses (
.pulse,.pulse-amber) and hover breathing (.breathe-link,.breathe-static) are static when reduced motion is preferred — via page-local or theme media query (these classes do not ship with built-in reduce blocks inforge-theme.css). - Parallax/tilt (
.ks-tilt-wrap,.ks-parallax-wrap) setstransition: none !importanton inner layers under reduced motion;js/ks-tilt-tiles.jsskips pointer tracking when the media query matches. - Autoplay video is absent, paused, or replaced with a static poster when
prefers-reduced-motion: reducematches. - No visible SVG SMIL on decorative diagrams, or SMIL is removed/paused under reduced motion (
js/ks-animated-backgrounds.jspauses injected SMIL when reduce is set). - Loading and progress indicators (
spinner-border,loading,skeleton,progress-ring,role="status",aria-busy="true") may keep motion — the check treats them as essential. reducedMotionCssRuleCount> 0 in audit metrics when custom motion is present (stylesheet contains at least one reduced-motion media block).
Failing signals
.pulse-amber/.pulse(or custom@keyframes) still running with duration > 0.02s when reduced motion is emulated — common when a page adds decorative classes without a matching media override..breathe-link/.breathe-statichover transitions (0.25s in theme) or hover infinitebreathe-amber/breathe-cyananimations remain active on visible cards..forge-cardor link tiles keeptransition: all 0.2sbecause a consumer stylesheet reintroduces transitions without the theme's reduced-motion block..ks-tilt-innerstill animates transform under reduced motion when a page overrides theme CSS or omitsforge-theme.css.- Visible
<video autoplay>on hero or ambient bands while reduced motion is preferred. - Inline
<style>blocks that define infinite animation without a@media (prefers-reduced-motion: reduce)companion. - SVG
<animate>/<animateTransform>inside handbook diagrams — SMIL ignores CSSanimation: noneon the parent SVG. - Zero
prefers-reduced-motionmedia rules in stylesheets while the page ships custom motion primitives.
Before example
Before (failing example)
Decorative float and pulse keep running when Reduce motion is on.
Quickstart
Failing KS markup: always-on pulse accent, hover breathing without override, card transitions, autoplay video, SVG SMIL, and no reduced-motion media query.
After example
After (passing example)
Static copy — motion reserved for essential status only.
Quickstart
Passing KS markup: theme-aligned reduced-motion overrides, static decorative surfaces, essential spinner allowed, no autoplay video, static SVG diagram.
Evidence and remediation
Evidence: Auditor metrics field motionPrefersReducedReport with:
| Field | Meaning |
|---|---|
reducedMotionPreferred |
true when emulateMedia applied |
reducedMotionCssRuleCount |
Stylesheet @media (prefers-reduced-motion: reduce) rule count |
nonEssentialMotionCount |
Visible non-essential motion instances |
violations[] |
Up to 12 sampled violations with kind, selectorHint, durations |
Violation kinds:
| Kind | Severity | Meaning |
|---|---|---|
active-animation |
major | Non-essential animation-name with duration > 0.02s |
active-transition |
warn | Non-essential transition-duration > 0.02s |
autoplay-video |
warn | Visible <video autoplay> |
smil-animation |
major | SVG SMIL element inside visible SVG |
Each finding includes selectorHint (tag, classes, optional data-ks-hash), animation/transition metadata, and page URL.
Remediate (in order):
- Add theme-level overrides — mirror KS patterns:
animation: none !importantfor.pulse,.breathe-*, ambient shimmer, living-background drift;transition: nonefor.forge-card,.ks-tilt-inner,.breathe-*. - Scope page-local motion — wrap custom
@keyframesin@media (prefers-reduced-motion: no-preference)or provide areduceblock that setsanimation: none. - Remove or pause autoplay video under reduced motion; ship a static
<img>poster (as in the after example). - Replace SVG SMIL with static paths or pause via script when
matchMedia('(prefers-reduced-motion: reduce)').matches— CSS alone cannot stop external SMIL. - Keep essential indicators — use Bootstrap
.spinner-border,aria-busy="true", orrole="status"for real progress; do not strip loading motion users need. - Re-run
analyze-website-ux.mjsmetrics on affected URLs; pair withDET.MOTION.NO_AUTO_PLAY_FLASHwhen fast strobing remains after motion is gated.
Harness verification:
python3 generator/build_rule_defect_fixtures.py
auditor-tests/invoke-det-ruleset-harness.sh --only-rule DET.MOTION.PREFERS_REDUCED