Rule handbook metadata

Rule
DET.MOTION.PREFERS_REDUCED · lane deterministic
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.py copies the After example HTML from this handbook page onto the defect fixture, then invoke-det-ruleset-remediation-verify.sh re-audits and expects zero findings for this rule.
  • Production sites: run-website-ux-remediation-loop.sh runs handbook_html_patch (HTML patches in lib/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: none or near-zero duration) and transitions (transition: none or ≤ 0.02s) — see KS patterns in forge-theme.css (.forge-card, .ks-tilt-inner), forge-ambient.css (.fa-css-shimmer), ks-living-background.css (.ks-living-drift-layer), and script-assembly.css (.ks-sa-root blanket 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 in forge-theme.css).
  • Parallax/tilt (.ks-tilt-wrap, .ks-parallax-wrap) sets transition: none !important on inner layers under reduced motion; js/ks-tilt-tiles.js skips pointer tracking when the media query matches.
  • Autoplay video is absent, paused, or replaced with a static poster when prefers-reduced-motion: reduce matches.
  • No visible SVG SMIL on decorative diagrams, or SMIL is removed/paused under reduced motion (js/ks-animated-backgrounds.js pauses 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-static hover transitions (0.25s in theme) or hover infinite breathe-amber / breathe-cyan animations remain active on visible cards.
  • .forge-card or link tiles keep transition: all 0.2s because a consumer stylesheet reintroduces transitions without the theme's reduced-motion block.
  • .ks-tilt-inner still animates transform under reduced motion when a page overrides theme CSS or omits forge-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 CSS animation: none on the parent SVG.
  • Zero prefers-reduced-motion media 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.

Path

Quickstart

24
Active rules
SMIL ignores CSS animation:none

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.

Path

Quickstart

24
Syncing rules
Static SVG — no SMIL
Static poster frame replacing autoplay video under reduced motion

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):

  1. Add theme-level overrides — mirror KS patterns: animation: none !important for .pulse, .breathe-*, ambient shimmer, living-background drift; transition: none for .forge-card, .ks-tilt-inner, .breathe-*.
  2. Scope page-local motion — wrap custom @keyframes in @media (prefers-reduced-motion: no-preference) or provide a reduce block that sets animation: none.
  3. Remove or pause autoplay video under reduced motion; ship a static <img> poster (as in the after example).
  4. Replace SVG SMIL with static paths or pause via script when matchMedia('(prefers-reduced-motion: reduce)').matches — CSS alone cannot stop external SMIL.
  5. Keep essential indicators — use Bootstrap .spinner-border, aria-busy="true", or role="status" for real progress; do not strip loading motion users need.
  6. Re-run analyze-website-ux.mjs metrics on affected URLs; pair with DET.MOTION.NO_AUTO_PLAY_FLASH when 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