Context
packages/loopover-ui-kit and apps/loopover-miner-ui already have an established, working convention for respecting prefers-reduced-motion: button.tsx (motion-reduce:transition-none motion-reduce:active:scale-100), skeleton.tsx (animate-pulse ... motion-reduce:animate-none, fixed in #7016), state-views.tsx (animate-spin ... motion-reduce:animate-none, explicitly documented as "All animations respect prefers-reduced-motion"), and tabs.tsx (motion-reduce:transition-none) all pair their animation utility with a motion-reduce: variant that disables it.
A large batch of other animated components never got the same treatment and still animate unconditionally for a user with the OS-level "reduce motion" preference set:
packages/loopover-ui-kit/src/components/ (Radix data-[state=open]:animate-in / data-[state=closed]:animate-out transition families, plus two standalone animations):
accordion.tsx — data-[state=closed]:animate-accordion-up data-[state=open]:animate-accordion-down
alert-dialog.tsx — overlay fade + content zoom/fade
context-menu.tsx — content zoom/fade/slide (2 call sites: ContextMenuContent, ContextMenuSubContent)
dialog.tsx — overlay fade + content zoom/fade
dropdown-menu.tsx — content zoom/fade/slide (2 call sites: DropdownMenuContent, DropdownMenuSubContent)
hover-card.tsx — content zoom/fade/slide
input-otp.tsx — animate-caret-blink on the fake caret
menubar.tsx — content zoom/fade/slide (2 call sites: MenubarContent, MenubarSubContent)
navigation-menu.tsx — viewport + indicator + content transitions (3 call sites)
popover.tsx — content zoom/fade/slide
select.tsx — content zoom/fade/slide
sheet.tsx — overlay fade + content slide
tooltip.tsx — content zoom/fade/slide
apps/loopover-miner-ui/src/components/:
streaming-text.tsx — the trailing cursor's animate-pulse
chat/typing-indicator.tsx — the three animate-bounce dots
None of these 15 files pair their animate-* utility with a motion-reduce: variant, so a user who has set "reduce motion" at the OS level still gets full fade/zoom/slide/bounce/blink/caret animation from every menu, dialog, tooltip, popover, and chat affordance in this app — exactly the class of bug #7016 already fixed for Skeleton and #7015 fixed for AccordionTrigger's missing focus ring (same "unlike every other X in ui-kit" shape).
Requirements
- For every
animate-in / animate-out / animate-accordion-up / animate-accordion-down / animate-caret-blink / animate-pulse / animate-bounce class list identified above, add the matching motion-reduce: variant that disables the animation (motion-reduce:animate-none for a plain animation utility; for the Radix animate-in/animate-out compound utility, disable it the same way skeleton.tsx/state-views.tsx already do — do not invent a different mechanism).
- Do not change any non-motion-related class (color, spacing, sizing) on these lines — this is a targeted
motion-reduce: addition only, not a restyle.
- Do not add a new shared "animated" wrapper/abstraction — apply the fix inline at each call site, mirroring
skeleton.tsx/button.tsx/tabs.tsx's existing pattern exactly.
- Every file listed above must be touched; a PR that fixes only a subset of the 15 files does not resolve this issue.
Deliverables
Test Coverage Requirements
packages/loopover-ui-kit is not in the root vitest.config.ts's coverage.include and is not Codecov-gated (see packages/loopover-ui-kit/vitest.config.ts's own comment: "the acceptance signal is that this suite runs and passes, not a percentage") — add tests there, but Codecov's patch gate will not enforce them.
apps/loopover-miner-ui is excluded from the root Codecov config (codecov.yml's ignore: - "apps/**") but has its own local vitest coverage floor enforced via apps/loopover-miner-ui/vitest.config.ts's coverage.thresholds (currently 85% statements / 85% branches / 75% functions / 85% lines, checked by npm test in that workspace) — a PR touching streaming-text.tsx / typing-indicator.tsx must not regress that local floor, and should add assertions for the new motion-reduce: classes to streaming-text.test.tsx / a typing-indicator.test.tsx (new file — none exists today for this component).
Expected Outcome
Every animated ui-kit/miner-ui component listed above stops animating when the OS "reduce motion" preference is set, matching the accessibility guarantee state-views.tsx already documents for its own animations and that skeleton.tsx/button.tsx/tabs.tsx already provide.
Links & Resources
Context
packages/loopover-ui-kitandapps/loopover-miner-uialready have an established, working convention for respectingprefers-reduced-motion:button.tsx(motion-reduce:transition-none motion-reduce:active:scale-100),skeleton.tsx(animate-pulse ... motion-reduce:animate-none, fixed in #7016),state-views.tsx(animate-spin ... motion-reduce:animate-none, explicitly documented as "All animations respect prefers-reduced-motion"), andtabs.tsx(motion-reduce:transition-none) all pair their animation utility with amotion-reduce:variant that disables it.A large batch of other animated components never got the same treatment and still animate unconditionally for a user with the OS-level "reduce motion" preference set:
packages/loopover-ui-kit/src/components/(Radixdata-[state=open]:animate-in/data-[state=closed]:animate-outtransition families, plus two standalone animations):accordion.tsx—data-[state=closed]:animate-accordion-up data-[state=open]:animate-accordion-downalert-dialog.tsx— overlay fade + content zoom/fadecontext-menu.tsx— content zoom/fade/slide (2 call sites:ContextMenuContent,ContextMenuSubContent)dialog.tsx— overlay fade + content zoom/fadedropdown-menu.tsx— content zoom/fade/slide (2 call sites:DropdownMenuContent,DropdownMenuSubContent)hover-card.tsx— content zoom/fade/slideinput-otp.tsx—animate-caret-blinkon the fake caretmenubar.tsx— content zoom/fade/slide (2 call sites:MenubarContent,MenubarSubContent)navigation-menu.tsx— viewport + indicator + content transitions (3 call sites)popover.tsx— content zoom/fade/slideselect.tsx— content zoom/fade/slidesheet.tsx— overlay fade + content slidetooltip.tsx— content zoom/fade/slideapps/loopover-miner-ui/src/components/:streaming-text.tsx— the trailing cursor'sanimate-pulsechat/typing-indicator.tsx— the threeanimate-bouncedotsNone of these 15 files pair their
animate-*utility with amotion-reduce:variant, so a user who has set "reduce motion" at the OS level still gets full fade/zoom/slide/bounce/blink/caret animation from every menu, dialog, tooltip, popover, and chat affordance in this app — exactly the class of bug #7016 already fixed forSkeletonand #7015 fixed forAccordionTrigger's missing focus ring (same "unlike every other X in ui-kit" shape).Requirements
animate-in/animate-out/animate-accordion-up/animate-accordion-down/animate-caret-blink/animate-pulse/animate-bounceclass list identified above, add the matchingmotion-reduce:variant that disables the animation (motion-reduce:animate-nonefor a plain animation utility; for the Radixanimate-in/animate-outcompound utility, disable it the same wayskeleton.tsx/state-views.tsxalready do — do not invent a different mechanism).motion-reduce:addition only, not a restyle.skeleton.tsx/button.tsx/tabs.tsx's existing pattern exactly.Deliverables
motion-reduce:variants added to all 13packages/loopover-ui-kit/src/components/*.tsxfiles listed in Contextmotion-reduce:variants added toapps/loopover-miner-ui/src/components/streaming-text.tsxandapps/loopover-miner-ui/src/components/chat/typing-indicator.tsxmotion-reduce:counterpart — e.g. a class-list assertion on rendered output for one Radix content component and forTypingIndicator, following the patternstate-views.test.tsxortheme-toggle.test.tsxalready use for class-list assertionsTest Coverage Requirements
packages/loopover-ui-kitis not in the rootvitest.config.ts'scoverage.includeand is not Codecov-gated (seepackages/loopover-ui-kit/vitest.config.ts's own comment: "the acceptance signal is that this suite runs and passes, not a percentage") — add tests there, but Codecov's patch gate will not enforce them.apps/loopover-miner-uiis excluded from the root Codecov config (codecov.yml'signore: - "apps/**") but has its own local vitest coverage floor enforced viaapps/loopover-miner-ui/vitest.config.ts'scoverage.thresholds(currently 85% statements / 85% branches / 75% functions / 85% lines, checked bynpm testin that workspace) — a PR touchingstreaming-text.tsx/typing-indicator.tsxmust not regress that local floor, and should add assertions for the newmotion-reduce:classes tostreaming-text.test.tsx/ atyping-indicator.test.tsx(new file — none exists today for this component).Expected Outcome
Every animated ui-kit/miner-ui component listed above stops animating when the OS "reduce motion" preference is set, matching the accessibility guarantee
state-views.tsxalready documents for its own animations and thatskeleton.tsx/button.tsx/tabs.tsxalready provide.Links & Resources
packages/loopover-ui-kit/src/components/skeleton.tsx(themotion-reduce:animate-nonepattern to mirror, fixed in Skeleton's animate-pulse doesn't respect prefers-reduced-motion, unlike every other animated ui-kit primitive #7016)packages/loopover-ui-kit/src/components/state-views.tsx(Spinner'smotion-reduce:animate-none+ doc comment)packages/loopover-ui-kit/src/components/button.tsxandpackages/loopover-ui-kit/src/components/tabs.tsx(motion-reduce:transition-nonepattern)Skeleton)AccordionTriggerfocus-visible — same "unlike every other X in ui-kit" framing)