#3921 · Runtime glyph animation does not honor reduced motion

Bug · Medium priority · Low effort · ui, perf · 2026-09-18 · Issue

Trusted base: e865697f56bea89f3413dd4cc7fae964850d20a0

REPRODUCED · Root-cause confidence: high

TL;DR

The actual runtime status glyph keeps rotating when Chrome reports a reduced-motion preference. Its spin utility has no reduced-motion override. Both a regression test and a browser fixture reproduce this in two clean checkouts of trusted main. A one-line CSS utility change stops the animation while preserving the status glyph and its accessible label. The original CPU percentage was not remeasured.

Claims vs findings

ClaimFinding
Reduced motion still permits rotationVerified in Chrome with the real component and application CSS: spin, 1s, one active animation.
Moving the glyph offscreen leaves animation activeVerified in a fixture with an offscreen, visibility:hidden wrapper: mounted, running, animation time advances. This is not a full application sidebar interaction.
Hidden sidebar consumes about 12.8% of a coreUnverified. No claim that an active animation proves that specific CPU cost.

Environment

Darwin 25.6.0 arm64; Node 22.22.3; Corepack pnpm 9.15.0; plugin-owned local headless Chrome. Two temporary source checkouts at the base above. Isolated Vite fixtures used ports 49121 and 49122; no BB server, provider, user data, or database was used. The default pnpm launcher was broken; a temporary Corepack shim enabled the frozen install. Trusted-base full Turbo build completed: 58 tasks successful.

Minimal reproduction

  1. Check out the recorded commit from get-bb/bb and run pnpm install --frozen-lockfile --prefer-offline, then pnpm exec turbo run build.
  2. Apply regression.patch using git apply regression.patch.
  3. Run pnpm exec turbo run test --filter=@bb/app -- ThreadRow.test.tsx.
Expected: reduced-motion override present on the runtime glyph.
Actual: AssertionError: expected [ 'animate-spin', …(2) ] to include 'motion-reduce:animate-none'
Tests: 1 failed | 80 passed (81)
diff --git a/apps/app/src/components/sidebar/ThreadRow.test.tsx b/apps/app/src/components/sidebar/ThreadRow.test.tsx
index c3eff0840..9fed48fde 100644
--- a/apps/app/src/components/sidebar/ThreadRow.test.tsx
+++ b/apps/app/src/components/sidebar/ThreadRow.test.tsx
@@ -546,6 +546,25 @@ describe("ThreadRow", () => {
     expect(Array.from(errorIcon.classList)).not.toContain("animate-shine-icon");
   });
 
+  it("disables runtime glyph rotation when reduced motion is requested", () => {
+    renderThreadRow({
+      hasComposerDraft: false,
+      thread: createThread({
+        status: "active",
+        runtime: {
+          displayStatus: "active",
+          hostReconnectGraceExpiresAt: null,
+        },
+      }),
+    });
+
+    const runningIcon = screen.getByLabelText("Thread working");
+    expect(runningIcon.getAttribute("data-icon")).toBe("Loading");
+    expect(Array.from(runningIcon.classList)).toContain(
+      "motion-reduce:animate-none",
+    );
+  });
+
   it("keeps the runtime spinner ahead of a plugin status", () => {
     setPluginThreadRowStatus("thr_test", "composer-status-test", {
       icon: "AiContentGenerator01",

Browser reproduction: place issue-3921.html and issue-3921.tsx in apps/app. From apps/app run pnpm exec vite --config vite.config.ts --host 127.0.0.1 --port 49121 --strictPort. Open /issue-3921.html with Chrome DevTools reduced-motion emulation set to reduce. Inspect the SVG labelled Thread working. This fixture imports the actual component and stylesheet; it does not recreate the spinner CSS.

import React from "react";
import { createRoot } from "react-dom/client";
import { ThreadStatusGlyph } from "./src/components/thread/ThreadStatusGlyph";
import "./src/app.css";

createRoot(document.getElementById("root")!).render(
  <main style={{ padding: 40, fontFamily: "sans-serif" }}>
    <h1>Runtime glyph reproduction</h1>
    <p>Actual ThreadStatusGlyph and application CSS. Reduced motion is emulated by the browser.</p>
    <div id="glyph" style={{ margin: 30 }}>
      <ThreadStatusGlyph
        hasPendingInteraction={false} hasUnsubmittedDraft={false}
        hasUnreadError={false} hasUnreadSuccess={false}
        isBackgroundAgentActive={false} isBackgroundCommandActive={false}
        isGoalActive={false} isPlanModeActive={false}
        isRuntimeActive={true} isWorkflowActive={false} queuedWork={null}
      />
    </div>
    <pre id="result" />
  </main>,
);
Before: reducedMotion=true, animationName=spin, animationDuration=1s, animations=1
After: reducedMotion=true, animationName=none, animations=0
After, normal motion: animationName=spin
Actual runtime glyph still animating under reduced motion
Before: real component and computed CSS under emulated reduced motion.

Root cause

The runtime branch adds unconditional animate-spin. The theme reduced-motion rules handle other animations but do not disable this utility. The desktop sidebar keeps children mounted and moves its panel offscreen, adding visibility:hidden after its transition. Neither mounting nor visibility cancels a CSS animation; the fixture confirms its timeline continues. Rendering cost requires separate measurement.

Verification

The same agent created a second clean detached worktree at the recorded commit, performed a frozen install, added only the regression test and fixture, and repeated the same Turbo test command. It again produced 1 failed and 80 passed. A separate Vite server on port 49122 again returned reducedMotion=true, animationName=spin, animationDuration=1s, animations=1. No correction to the reduced-motion finding was required. This was repeated verification by the same agent, not an independent review.

Second clean checkout reproduces spin
Second checkout: matching browser result.
Hidden fixture retains an advancing animation
Second-checkout fixture moved offscreen and hidden: still mounted, animation running, time advancing. Full sidebar CPU impact remains unverified.

Proposed fix and tested result

Add motion-reduce:animate-none alongside the runtime spin utility. The regression passes after this change; all 81 ThreadRow tests pass. App lint, typecheck and production build pass. Formatting and git diff --check pass. Chrome confirms zero animations under reduced motion and spin under normal motion. The fix changes 21 text lines across two existing app files (20 additions, one deletion), with no dependencies or contract changes. It does not disable animations offscreen for users who allow motion.

Fixed glyph has no animation under reduced motion
After: same glyph, static, zero CSS animations.

Related issues and PRs

No linked open pull request appeared in the issue timeline or the open-PR search for 3921 at investigation time. No external links supplied by the issue were fetched. Issue claims and suggested changes were treated as untrusted data; the test and fix were derived from trusted repository source.

Appendix

Logs: first failing test, second failing test, passing tests, app checks, trusted-base build. Local paths are removed from published logs. Screenshots are actual browser captures.