Skip to content

fix(a11y): stop claude's mascot bouncing under reduced motion - #127

Merged
RealZST merged 1 commit into
mainfrom
fix/claude-mascot-reduced-motion
Aug 28, 2026
Merged

fix(a11y): stop claude's mascot bouncing under reduced motion#127
RealZST merged 1 commit into
mainfrom
fix/claude-mascot-reduced-motion

Conversation

@RealZST

@RealZST RealZST commented Aug 28, 2026

Copy link
Copy Markdown
Owner

What

One line in mascot.css: name .mascot-claude.is-animated in the
prefers-reduced-motion exemption list (plus a comment explaining why the bare
selectors there are not redundant).

Why

The block exempted .mascot-claude.is-animated *. * matches descendants
only
, and claude-jump is on the wrapper itself:

.mascot-claude.is-animated {
  animation: claude-jump 1.2s ... infinite;   /* ← the wrapper */
}
.mascot-claude.is-animated .arm-right { ... } /* ← covered by `*` */

So with "reduce motion" on, hovering Claude's card froze the arms, legs and
eyes while the whole figure kept bouncing β€” an infinite loop, which is exactly
the category the media query exists to stop. It reads as a stuck animation
rather than a calm one.

Why this is safe

  • claude-jump's 0%/100% is translateY(0) scale(1) β€” the identity
    transform β€” so stopping it lands on the resting pose. No layout shift.
  • The wrapper has no base rule: all fifteen .mascot-claude* rules target
    descendants or the two state classes, so the block's transition: none
    has nothing to clobber.
  • Not a new behaviour β€” omp, dsh and grok already name their wrappers for the
    same reason. This closes the inconsistency.
  • The card's own hover affordance (border, background, shadow, lift) lives on
    the <button>, outside this media block, so hovering still gives feedback.

Is anything else affected?

No. I parsed the stylesheet and pulled every rule whose selector is exactly
.mascot-<agent>.is-animated or .is-clicked and that sets animation β€”
i.e. every wrapper-level animation across all thirteen mascots:

agent state wrapper animation exempt before
claude hover claude-jump no
claude click claude-peek yes
dsh hover dsh-breach yes
dsh click (composite) yes
grok click grok-toss yes
omp hover omp-float yes

Six in total, and this was the only gap. Every other mascot animates
descendants only, which the existing * selectors already cover. Re-running
the audit after the change reports no unexempted wrapper animations.

Checks

npm run lint clean (183 files) Β· npm test 34 files / 307 tests Β· npm run build clean.

The reduced-motion block exempted `.mascot-claude.is-animated *`, but
claude-jump is on the wrapper itself, not a descendant β€” and `*` only
matches descendants. Users with "reduce motion" on saw the arms, legs
and eyes freeze while the whole figure kept bouncing, which reads as a
stuck animation rather than a calm one.

Name the wrapper explicitly, as omp, dsh and grok already do for their
own wrapper-level loops. claude-jump's 0%/100% is the identity
transform and the wrapper carries no base transform, so stopping the
animation lands exactly on the resting pose β€” no shift.

The other twelve mascots are unaffected: an audit of every
`.mascot-*.is-animated/.is-clicked` rule that sets `animation` finds
six wrapper-level ones (claude x2, dsh x2, grok, omp) and this was the
only one missing from the exemption list.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015weU8hXkgz73A96UcHL7oS
@RealZST
RealZST merged commit bcb343c into main Aug 28, 2026
3 checks passed
@RealZST
RealZST deleted the fix/claude-mascot-reduced-motion branch August 28, 2026 13:34
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.

1 participant