Skip to content

Commit 7035d2f

Browse files
LEANDERANTONYclaude
andcommitted
fix(low): scope voice-input reduced-motion to a class (L6)
Background: the recording indicator's pulse was applied via an inline `animation` style, and the prefers-reduced-motion override matched it with span[style*="animation"] — a brittle attribute-substring selector. Any refactor moving the animation to a class or a different property would silently re-enable the pulsing dot for reduced-motion users (WCAG 2.3.3 / 2.2.2). Fix: drive the pulse with a `voice-input-pulsing` class (styled-jsx scoped) and point the reduced-motion rule at that class, so the opt-out tracks the actual animation hook rather than a string match on the style attribute. Test: tsc + eslint clean. Pure markup/CSS change with no behavioral branch to unit-test; covered by the typecheck/lint gates per the frontend discipline. Fixes: L6 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1 parent a4239c8 commit 7035d2f

1 file changed

Lines changed: 16 additions & 12 deletions

File tree

frontend/src/components/workspace/VoiceInputButton.tsx

Lines changed: 16 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -354,31 +354,32 @@ export function VoiceInputButton({
354354
}}
355355
>
356356
{isRecording ? (
357-
// Pulsing red dot while recording. CSS keyframes in the
358-
// existing globals.css don't include a `pulse` animation yet
359-
// — inline keyframes via a style tag would over-complicate
360-
// the surface. We rely on a subtle scale via inline style
361-
// animation so a user can't tell whether the recording is
362-
// active just from a static screenshot.
357+
// Pulsing red dot while recording — a subtle scale so the active
358+
// state reads even on a static screenshot. The animation is driven
359+
// by the `voice-input-pulsing` CLASS (review L6), not an inline
360+
// `animation` style, so the prefers-reduced-motion override can
361+
// target the class instead of a brittle [style*="animation"]
362+
// attribute-substring match that any refactor would silently break.
363363
<span
364+
className="voice-input-pulsing"
364365
style={{
365366
display: "inline-block",
366367
width: 10,
367368
height: 10,
368369
borderRadius: "50%",
369370
background: "#ef4444",
370-
animation: "voice-pulse 1.2s ease-in-out infinite",
371371
}}
372372
/>
373373
) : (
374374
<MicIcon />
375375
)}
376376
<span>{buttonLabel}</span>
377377
</span>
378-
{/* Keyframes defined inline so the button is self-contained;
379-
adding a new global rule for one component would bloat the
380-
stylesheet. The animation reduces gracefully for users with
381-
`prefers-reduced-motion` via the media query below. */}
378+
{/* Keyframes + the pulsing class defined inline so the button is
379+
self-contained; adding a new global rule for one component would
380+
bloat the stylesheet. The reduced-motion override targets the
381+
`.voice-input-pulsing` class (review L6) so it can't be defeated by
382+
a refactor that moves the animation off an inline style. */}
382383
<style jsx>{`
383384
@keyframes voice-pulse {
384385
0%, 100% {
@@ -390,8 +391,11 @@ export function VoiceInputButton({
390391
opacity: 0.6;
391392
}
392393
}
394+
.voice-input-pulsing {
395+
animation: voice-pulse 1.2s ease-in-out infinite;
396+
}
393397
@media (prefers-reduced-motion: reduce) {
394-
span[style*="animation"] {
398+
.voice-input-pulsing {
395399
animation: none !important;
396400
}
397401
}

0 commit comments

Comments
 (0)