From a424ee7d04bb78c75726dc5d3fa44cfef798b01b Mon Sep 17 00:00:00 2001 From: melloware Date: Tue, 28 Jul 2026 08:40:36 -0400 Subject: [PATCH 1/2] Fix #44: Prefer reduced motion for animations and transitions Co-authored-by: Cursor --- .../2026-07-28-prefers-reduced-motion.md | 253 ++++++++++++++++++ ...026-07-28-prefers-reduced-motion-design.md | 82 ++++++ pages/animationduration/index.js | 5 +- pages/animations/index.js | 6 +- pages/transitionduration/index.js | 5 +- styles/lib/core/_reducedmotion.scss | 33 +++ styles/lib/primeflex.scss | 1 + 7 files changed, 382 insertions(+), 3 deletions(-) create mode 100644 docs/superpowers/plans/2026-07-28-prefers-reduced-motion.md create mode 100644 docs/superpowers/specs/2026-07-28-prefers-reduced-motion-design.md create mode 100644 styles/lib/core/_reducedmotion.scss diff --git a/docs/superpowers/plans/2026-07-28-prefers-reduced-motion.md b/docs/superpowers/plans/2026-07-28-prefers-reduced-motion.md new file mode 100644 index 0000000..fb8d18b --- /dev/null +++ b/docs/superpowers/plans/2026-07-28-prefers-reduced-motion.md @@ -0,0 +1,253 @@ +# Prefers-reduced-motion Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Soften Mantle Flex animation and transition utilities under `prefers-reduced-motion: reduce` by capping durations at 150ms and forcing infinite animations to play once. + +**Architecture:** Add a dedicated `_reducedmotion.scss` partial that overrides only Mantle Flex utility/named-animation classes inside `@media (prefers-reduced-motion: reduce)`. Import it last in `primeflex.scss` so overrides win. Do not rewrite keyframes or use a global `*` selector. + +**Tech Stack:** Sass (Dart Sass via `sass` CLI), existing Mantle Flex utility class generation (`$prefix`, `!important` via `style-class`), Next.js docs site. + +**Spec:** `docs/superpowers/specs/2026-07-28-prefers-reduced-motion-design.md` + +## Global Constraints + +- Soften motion (cap), do not hard-disable +- Cap duration: `150ms` via `$reducedMotionDuration: 150ms !default` +- Scope: Mantle Flex classes only (no universal selector) +- Keep transforms/keyframes unchanged +- Force `animation-iteration-infinite` → `1` +- Overrides must use `!important` to beat existing duration utilities +- No opt-out class in v1 +- No interactive reduced-motion demo in v1 + +--- + +## File Structure + +| File | Responsibility | +|---|---| +| `styles/lib/core/_reducedmotion.scss` | Media-query overrides for duration + infinite iteration | +| `styles/lib/primeflex.scss` | Import `_reducedmotion` after `_animation` | +| `pages/animations/index.js` | Doc intro note about reduced motion | +| `pages/animationduration/index.js` | Doc intro note about duration capping | +| `pages/transitionduration/index.js` | Doc intro note about transition capping | + +--- + +### Task 1: Reduced-motion SCSS overrides + +**Files:** +- Create: `styles/lib/core/_reducedmotion.scss` +- Modify: `styles/lib/primeflex.scss` + +**Interfaces:** +- Consumes: `$prefix` from `styles/lib/core/_variables.scss` (already imported by `primeflex.scss`) +- Produces: `$reducedMotionDuration` (`150ms` default); media-query rules for duration utilities, named long animations, and infinite iteration + +- [ ] **Step 1: Create `_reducedmotion.scss`** + +Create `styles/lib/core/_reducedmotion.scss` with this exact content: + +```scss +$reducedMotionDuration: 150ms !default; + +@media (prefers-reduced-motion: reduce) { + .#{$prefix}animation-duration-200, + .#{$prefix}animation-duration-300, + .#{$prefix}animation-duration-400, + .#{$prefix}animation-duration-500, + .#{$prefix}animation-duration-1000, + .#{$prefix}animation-duration-2000, + .#{$prefix}animation-duration-3000 { + animation-duration: $reducedMotionDuration !important; + } + + .#{$prefix}transition-duration-200, + .#{$prefix}transition-duration-300, + .#{$prefix}transition-duration-400, + .#{$prefix}transition-duration-500, + .#{$prefix}transition-duration-1000, + .#{$prefix}transition-duration-2000, + .#{$prefix}transition-duration-3000 { + transition-duration: $reducedMotionDuration !important; + } + + .#{$prefix}slidedown, + .#{$prefix}slideup, + .#{$prefix}animate-width { + animation-duration: $reducedMotionDuration !important; + } + + .#{$prefix}animation-iteration-infinite { + animation-iteration-count: 1 !important; + } +} +``` + +- [ ] **Step 2: Import the partial last in `primeflex.scss`** + +In `styles/lib/primeflex.scss`, after the `_animation` import, add: + +```scss +@import './core/_reducedmotion'; +``` + +The end of the import list should look like: + +```scss +@import './core/_transition'; +@import './core/_transform'; +@import './core/_animation'; +@import './core/_reducedmotion'; +@import './core/_utils'; +``` + +(`_utils` may remain last if it already is — place `_reducedmotion` immediately after `_animation` and before `_utils`.) + +- [ ] **Step 3: Build the library CSS** + +Run: + +```bash +npm run build:sass +``` + +Expected: exit code 0; writes/updates `dist-lib/primeflex.css` (and minified sibling). + +If `dist-lib` is missing or the script fails because of prior steps, run: + +```bash +npx sass --update styles/lib/primeflex.scss:dist-lib/primeflex.css --no-source-map +``` + +Expected: exit code 0. + +- [ ] **Step 4: Verify media-query output** + +Run (PowerShell): + +```powershell +Select-String -Path dist-lib/primeflex.css -Pattern "prefers-reduced-motion" -Context 0,25 +``` + +Expected matches in the output CSS: + +- `@media (prefers-reduced-motion: reduce)` +- `.animation-duration-200` (and other capped animation duration classes) with `animation-duration: 150ms !important` +- `.transition-duration-200` (and other capped transition duration classes) with `transition-duration: 150ms !important` +- `.slidedown`, `.slideup`, `.animate-width` with `animation-duration: 150ms !important` +- `.animation-iteration-infinite` with `animation-iteration-count: 1 !important` + +Also confirm these are **not** overridden in the media query: + +- `.animation-duration-100` +- `.animation-duration-150` +- `.transition-duration-100` +- `.transition-duration-150` + +- [ ] **Step 5: Commit** + +```bash +git add styles/lib/core/_reducedmotion.scss styles/lib/primeflex.scss +git commit -m "feat: soften animations under prefers-reduced-motion" +``` + +--- + +### Task 2: Documentation notes + +**Files:** +- Modify: `pages/animations/index.js` +- Modify: `pages/animationduration/index.js` +- Modify: `pages/transitionduration/index.js` + +**Interfaces:** +- Consumes: reduced-motion behavior from Task 1 (150ms cap, infinite → once) +- Produces: short intro copy on three doc pages (no new components) + +- [ ] **Step 1: Update Animations intro** + +In `pages/animations/index.js`, replace the intro paragraph: + +```jsx +

A variety of animations are available to be used when an element enters or leaves.

+``` + +with: + +```jsx +

+ A variety of animations are available to be used when an element enters or leaves. When{' '} + prefers-reduced-motion: reduce is active, Mantle Flex caps animation durations at 150ms and forces + infinite animations to run once. +

+``` + +- [ ] **Step 2: Update Animation Duration intro** + +In `pages/animationduration/index.js`, replace: + +```jsx +

Defines how long an animation should take to complete.

+``` + +with: + +```jsx +

+ Defines how long an animation should take to complete. Under{' '} + prefers-reduced-motion: reduce, utilities above 150ms are capped at 150ms. +

+``` + +- [ ] **Step 3: Update Transition Duration intro** + +In `pages/transitionduration/index.js`, replace: + +```jsx +

Defines how long a transition should take to complete.

+``` + +with: + +```jsx +

+ Defines how long a transition should take to complete. Under{' '} + prefers-reduced-motion: reduce, utilities above 150ms are capped at 150ms. +

+``` + +- [ ] **Step 4: Sanity-check docs pages compile** + +Run: + +```bash +npm run lint +``` + +Expected: exit code 0 (or no new lint errors in the three edited files). + +- [ ] **Step 5: Commit** + +```bash +git add pages/animations/index.js pages/animationduration/index.js pages/transitionduration/index.js +git commit -m "docs: note prefers-reduced-motion duration capping" +``` + +--- + +## Spec coverage checklist + +| Spec requirement | Task | +|---|---| +| Cap animation-duration utilities >150ms | Task 1 | +| Cap transition-duration utilities >150ms | Task 1 | +| Cap slidedown / slideup / animate-width | Task 1 | +| Infinite → once | Task 1 | +| Keep keyframes/transforms | Task 1 (no keyframe edits) | +| `$reducedMotionDuration` !default | Task 1 | +| Import after animation/transition | Task 1 | +| Docs note | Task 2 | +| Build verification | Task 1 Steps 3–4 | +| No `*` selector / no opt-out / no demo | Honored by omission | diff --git a/docs/superpowers/specs/2026-07-28-prefers-reduced-motion-design.md b/docs/superpowers/specs/2026-07-28-prefers-reduced-motion-design.md new file mode 100644 index 0000000..de3523d --- /dev/null +++ b/docs/superpowers/specs/2026-07-28-prefers-reduced-motion-design.md @@ -0,0 +1,82 @@ +# Prefers-reduced-motion design + +**Issue:** [Mantle-UI/mantle-flex#44](https://github.com/Mantle-UI/mantle-flex/issues/44) +**Date:** 2026-07-28 +**Status:** Approved for implementation planning + +## Goal + +Respect the CSS media feature `prefers-reduced-motion: reduce` for Mantle Flex animation and transition utilities by **softening** motion (shorter duration, no infinite loops) rather than hard-disabling it. + +## Decisions + +| Topic | Choice | +|---|---| +| Strategy | Soften (cap duration), do not disable | +| Duration cap | `150ms` | +| Transforms / keyframes | Unchanged | +| Infinite animations | Force play once | +| Scope | Mantle Flex utility classes only | +| Transitions | Cap durations above 150ms at 150ms | +| Opt-out class | Out of scope for v1 | + +## Behavior + +When `@media (prefers-reduced-motion: reduce)` matches: + +1. **Animation duration utilities** above 150ms (`animation-duration-200` … `animation-duration-3000`) resolve to `150ms`. Classes already ≤150ms (`*-100`, `*-150`) stay as-is. +2. **Transition duration utilities** above 150ms (`transition-duration-200` … `transition-duration-3000`) resolve to `150ms`. Same rule for ≤150ms classes. +3. **Named animation classes** whose default duration exceeds the cap are capped: + - `slidedown` / `slideup` (`.45s`) → `150ms` + - `animate-width` (`1000ms`) → `150ms` + - Named animations already at `.15s` need no change. +4. **`animation-iteration-infinite`** → `animation-iteration-count: 1`. +5. Keyframes and transforms are **not** rewritten. + +## Non-goals (v1) + +- Global `*` / universal selector overrides +- Rewriting keyframes to opacity-only fades +- Opt-out utility (e.g. `motion-safe`) +- Changing delay utilities +- Interactive docs demo of reduced motion + +## Implementation + +### File layout + +- Add `styles/lib/core/_reducedmotion.scss` +- Import it last in `styles/lib/primeflex.scss` (after `_animation` and `_transition`) so overrides win cascade order + +### Override rules + +Redeclare only affected prefixed classes inside the media query. Use `!important` to match existing `style-class` duration utilities: + +```scss +$reducedMotionDuration: 150ms !default; + +@media (prefers-reduced-motion: reduce) { + // animation-duration-* and transition-duration-* above the cap → $reducedMotionDuration + // .slidedown, .slideup, .animate-width → animation-duration: $reducedMotionDuration + // .animation-iteration-infinite → animation-iteration-count: 1 +} +``` + +### Sass configurability + +Expose `$reducedMotionDuration: 150ms !default` so Sass consumers can retune the cap when compiling from source. + +### Documentation + +Add a short note on the Animations and/or Transition Duration doc pages describing that under `prefers-reduced-motion: reduce`, Mantle Flex caps utility durations at 150ms and forces infinite animations to run once. + +### Verification + +- `npm run build:lib` (or equivalent Sass build) succeeds +- Built `dist-lib/primeflex.css` contains the media query and expected selectors + +## Edge cases + +- Duration utilities already emit `!important`; reduced-motion overrides must also use `!important` to win. +- When a named animation class is combined with `animation-duration-*`, the capped utility value applies under reduce. +- App-owned CSS, inline styles, and non-Mantle classes are unaffected. diff --git a/pages/animationduration/index.js b/pages/animationduration/index.js index 57f3469..eb5fda8 100644 --- a/pages/animationduration/index.js +++ b/pages/animationduration/index.js @@ -29,7 +29,10 @@ const PositionPage = () => {

Animation Duration

-

Defines how long an animation should take to complete.

+

+ Defines how long an animation should take to complete. Under{' '} + prefers-reduced-motion: reduce, utilities above 150ms are capped at 150ms. +

diff --git a/pages/animations/index.js b/pages/animations/index.js index 98a3d49..5a0bcd4 100644 --- a/pages/animations/index.js +++ b/pages/animations/index.js @@ -161,7 +161,11 @@ const PositionPage = () => {

Animations

-

A variety of animations are available to be used when an element enters or leaves.

+

+ A variety of animations are available to be used when an element enters or leaves. When{' '} + prefers-reduced-motion: reduce is active, Mantle Flex caps animation durations at 150ms and forces + infinite animations to run once. +

diff --git a/pages/transitionduration/index.js b/pages/transitionduration/index.js index dbf6a57..eb11886 100644 --- a/pages/transitionduration/index.js +++ b/pages/transitionduration/index.js @@ -29,7 +29,10 @@ const PositionPage = () => {

Transition Duration

-

Defines how long a transition should take to complete.

+

+ Defines how long a transition should take to complete. Under{' '} + prefers-reduced-motion: reduce, utilities above 150ms are capped at 150ms. +

diff --git a/styles/lib/core/_reducedmotion.scss b/styles/lib/core/_reducedmotion.scss new file mode 100644 index 0000000..0707483 --- /dev/null +++ b/styles/lib/core/_reducedmotion.scss @@ -0,0 +1,33 @@ +$reducedMotionDuration: 150ms !default; + +@media (prefers-reduced-motion: reduce) { + .#{$prefix}animation-duration-200, + .#{$prefix}animation-duration-300, + .#{$prefix}animation-duration-400, + .#{$prefix}animation-duration-500, + .#{$prefix}animation-duration-1000, + .#{$prefix}animation-duration-2000, + .#{$prefix}animation-duration-3000 { + animation-duration: $reducedMotionDuration !important; + } + + .#{$prefix}transition-duration-200, + .#{$prefix}transition-duration-300, + .#{$prefix}transition-duration-400, + .#{$prefix}transition-duration-500, + .#{$prefix}transition-duration-1000, + .#{$prefix}transition-duration-2000, + .#{$prefix}transition-duration-3000 { + transition-duration: $reducedMotionDuration !important; + } + + .#{$prefix}slidedown, + .#{$prefix}slideup, + .#{$prefix}animate-width { + animation-duration: $reducedMotionDuration !important; + } + + .#{$prefix}animation-iteration-infinite { + animation-iteration-count: 1 !important; + } +} diff --git a/styles/lib/primeflex.scss b/styles/lib/primeflex.scss index 6776c8a..e4c5320 100644 --- a/styles/lib/primeflex.scss +++ b/styles/lib/primeflex.scss @@ -22,4 +22,5 @@ @import './core/_transition'; @import './core/_transform'; @import './core/_animation'; +@import './core/_reducedmotion'; @import './core/_utils'; \ No newline at end of file From d20be05ddd338643fe48a7313c5e335d42c23706 Mon Sep 17 00:00:00 2001 From: Melloware Date: Tue, 28 Jul 2026 08:46:21 -0400 Subject: [PATCH 2/2] Add import for reduced motion styles --- styles/lib/mantleflex.scss | 1 + 1 file changed, 1 insertion(+) diff --git a/styles/lib/mantleflex.scss b/styles/lib/mantleflex.scss index 435bf91..d6be1af 100644 --- a/styles/lib/mantleflex.scss +++ b/styles/lib/mantleflex.scss @@ -22,4 +22,5 @@ @import './core/_transition'; @import './core/_transform'; @import './core/_animation'; +@import './core/_reducedmotion'; @import './core/_utils';