Skip to content

fix(web): honor reduced-motion preferences across product UI - #4855

Open
FaisalMisbah23 wants to merge 3 commits into
Kilo-Org:mainfrom
FaisalMisbah23:fix/reduced-motion
Open

fix(web): honor reduced-motion preferences across product UI#4855
FaisalMisbah23 wants to merge 3 commits into
Kilo-Org:mainfrom
FaisalMisbah23:fix/reduced-motion

Conversation

@FaisalMisbah23

Copy link
Copy Markdown
Contributor

Summary

Adds a global @media (prefers-reduced-motion: reduce) CSS reset that kills all CSS animations and transitions when users enable reduced motion in their OS settings. Additionally, apply useReducedMotion() to Framer Motion animated components (HeaderLogo, BeadBoard, and OnboardingStepModel) to conditionally skip JS-driven animations.

Closes #4174

Reviewer Notes

The global CSS reset (*{ animation-duration: 0.01ms !important }) covers all CSS-based animations product-wide. Framer Motion components use the library's useReducedMotion() hook to conditionally skip animated props. Per-component changes are minimal the safety net does most of the work.

);
}

const shouldReduce = useReducedMotion();

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

CRITICAL: React Rules of Hooks violation - useReducedMotion() is called after an early return

useReducedMotion() is invoked here, but the component has an early return for the isLoading branch a few lines above (before this line). When isLoading is true, this hook is never called; when isLoading is false, it is called. Because the number of hooks invoked differs between renders of the same component instance whenever isLoading toggles, React will throw "Rendered fewer hooks than expected" / hook-order-mismatch errors and crash the component.

Move const shouldReduce = useReducedMotion(); to the top of the function body, before the if (isLoading) { ... } early return, so it is called unconditionally on every render.


Reply with @kilocode-bot fix it to have Kilo Code address this issue.

<div className="h-2 w-2 animate-bounce rounded-full bg-gray-400 [animation-delay:-0.3s]" />
<div className="h-2 w-2 animate-bounce rounded-full bg-gray-400 [animation-delay:-0.15s]" />
<div className="h-2 w-2 animate-bounce rounded-full bg-gray-400" />
<div className="motion-reduce:bg-gray-400 motion-reduce:opacity-40 h-2 w-2 animate-bounce rounded-full bg-gray-400 [animation-delay:-0.3s]" />

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

WARNING: motion-reduce:opacity-40 does not stop the bounce animation, and motion-reduce:bg-gray-400 is a no-op

Two issues with the added utility classes on these three dots:

  1. motion-reduce:bg-gray-400 sets the same background color that's already applied unconditionally (bg-gray-400), so it has no effect under reduced motion.
  2. animate-bounce itself is never disabled here (no motion-reduce:animate-none), so this change alone does not stop the animation. The bounce is only neutralized because of the unrelated global reset in apps/web/src/app/globals.css (animation-iteration-count: 1 !important / animation-duration: 0.01ms !important). If that global rule is ever scoped down or removed, these dots will keep bouncing despite this "fix". Consider adding motion-reduce:animate-none directly on these elements so the intent is explicit and self-contained.

Reply with @kilocode-bot fix it to have Kilo Code address this issue.

Comment thread apps/web/src/app/globals.css Outdated
*::before,
*::after {
animation-duration: 0.01ms !important;
animation-iteration-count: 1 !important;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

SUGGESTION: Global animation-iteration-count: 1 !important will freeze infinite/looping animations mid-frame rather than resolving them cleanly

This blanket reset forces every animation (including unbounded/looping ones like animate-spin loaders or animate-bounce indicators elsewhere in the app) to run once for 0.01ms and then stop on whatever frame that iteration lands on. For simple 360° rotations this happens to look static, but for any animation whose start/end keyframes differ, elements can appear to freeze on an intermediate/incorrect frame instead of settling into a clean resting state — which can look broken for reduced-motion users, especially for anything communicating an ongoing loading state. The PR already shows awareness of this trade-off by adding a dedicated .animate-pulse-once override; consider auditing other unbounded animations (loading spinners in particular) for the same treatment rather than relying on the global reset alone.


Reply with @kilocode-bot fix it to have Kilo Code address this issue.

@@ -0,0 +1,18 @@
import type { StoryObj } from '@storybook/react';

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

CRITICAL: New story file is in the wrong directory and missing the required default export - it will not be picked up by Storybook

Two problems:

  1. Storybook's config (apps/storybook/.storybook/main.ts) only globs ../stories/**/*.stories.@(js|jsx|mjs|ts|tsx), i.e. apps/storybook/stories/. This file lives in apps/web/src/stories/ instead, so Storybook will never discover it.
  2. Even if it were in the right location, Storybook's Component Story Format requires a default export with at least a title (see other files in apps/storybook/stories/*.stories.tsx, e.g. export default { title: '...' }). This file has no default export, so it would fail to register as a story module.

Move this file to apps/storybook/stories/ReducedMotion.stories.tsx and add a default export with a title (and typically a Meta type import), following the pattern used by the other story files in that directory.


Reply with @kilocode-bot fix it to have Kilo Code address this issue.

@kilo-code-bot

kilo-code-bot Bot commented Jul 29, 2026

Copy link
Copy Markdown
Contributor

Code Review Summary

Status: No Issues Found | Recommendation: Merge

Executive Summary

The final commit removes the duplicate, non-functional apps/web/src/stories/ReducedMotion.stories.tsx, resolving the last open finding; all previously identified issues in this PR are now fixed.

Files Reviewed (1 file)
  • apps/web/src/stories/ReducedMotion.stories.tsx - deleted, 0 issues (previous critical issue resolved)
Previous Review Summaries (2 snapshots, latest commit 946c350)

Current summary above is authoritative. Previous snapshots are kept for context only.

Previous review (commit 946c350)

Status: 1 Issue Found | Recommendation: Address before merge

Executive Summary

The follow-up commit correctly fixes the Rules-of-Hooks violation, the typing-indicator animation, and the global CSS reset, but it leaves a duplicate, still-broken story file behind at apps/web/src/stories/ReducedMotion.stories.tsx instead of removing/relocating it.

Overview

Severity Count
CRITICAL 1
WARNING 0
SUGGESTION 0
Issue Details (click to expand)

CRITICAL

File Line Issue
apps/web/src/stories/ReducedMotion.stories.tsx 1 Duplicate story file remains in the wrong directory (not matched by Storybook's stories glob) and still has no default export. The fix commit added a correct copy at apps/storybook/stories/ReducedMotion.stories.tsx instead of removing/relocating this one, leaving dead, non-functional duplicate content behind.
Resolved Since Last Review
  • apps/web/src/components/gastown/BeadBoard.tsx - useReducedMotion() moved before the early isLoading return, fixing the Rules of Hooks violation
  • apps/web/src/components/cloud-agent-next/TypingIndicator.tsx - replaced no-op utility classes with motion-reduce:animate-none, correctly disabling the bounce animation
  • apps/web/src/app/globals.css - removed the global animation-iteration-count: 1 !important rule, avoiding mid-frame freezes on looping animations
Files Reviewed in This Update (5 files)
  • apps/storybook/stories/ReducedMotion.stories.tsx - 0 issues (new file, correctly located with required default export)
  • apps/web/src/app/globals.css - 0 issues (previous suggestion resolved)
  • apps/web/src/components/cloud-agent-next/TypingIndicator.tsx - 0 issues (previous warning resolved)
  • apps/web/src/components/gastown/BeadBoard.tsx - 0 issues (previous critical issue resolved)
  • apps/web/src/stories/ReducedMotion.stories.tsx - 1 issue (unchanged, still open)

Fix these issues in Kilo Cloud

Previous review (commit 79b4d28)

Status: 4 Issues Found | Recommendation: Address before merge

Executive Summary

Highest-risk issue: BeadBoard.tsx calls useReducedMotion() after a conditional early return, which violates React's Rules of Hooks and can crash the component when isLoading toggles.

Overview

Severity Count
CRITICAL 2
WARNING 1
SUGGESTION 1
Issue Details (click to expand)

CRITICAL

File Line Issue
apps/web/src/components/gastown/BeadBoard.tsx 222 useReducedMotion() called after an early return, violating Rules of Hooks
apps/web/src/stories/ReducedMotion.stories.tsx 1 Story file in wrong directory (not matched by Storybook's stories glob) and missing required default export

WARNING

File Line Issue
apps/web/src/components/cloud-agent-next/TypingIndicator.tsx 12 motion-reduce:opacity-40 doesn't disable animate-bounce; fix relies entirely on an unrelated global CSS reset, and motion-reduce:bg-gray-400 is a no-op

SUGGESTION

File Line Issue
apps/web/src/app/globals.css 135 Global animation-iteration-count: 1 !important can freeze unbounded/looping animations mid-frame instead of resolving cleanly
Files Reviewed (7 files)
  • apps/web/src/app/(app)/gastown/onboarding/OnboardingStepModel.tsx - 0 issues
  • apps/web/src/app/globals.css - 1 issue
  • apps/web/src/components/HeaderLogo.tsx - 0 issues
  • apps/web/src/components/cloud-agent-next/TypingIndicator.tsx - 1 issue
  • apps/web/src/components/gastown/BeadBoard.tsx - 1 issue
  • apps/web/src/components/ui/sidebar.tsx - 0 issues
  • apps/web/src/stories/ReducedMotion.stories.tsx - 1 issue

Fix these issues in Kilo Cloud


Reviewed by claude-sonnet-5 · Input: 26 · Output: 4.8K · Cached: 515.8K

Review guidance: REVIEW.md from base branch main

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

fix(web): honor reduced-motion preferences across product UI

1 participant