Skip to content

Checkbox: respect prefers-reduced-motion for the checkmark animation - #8647

Open
adityalenova wants to merge 1 commit into
primer:mainfrom
adityalenova:fix/checkbox-reduced-motion
Open

adityalenova wants to merge 1 commit into
primer:mainfrom
adityalenova:fix/checkbox-reduced-motion

Conversation

@adityalenova

Copy link
Copy Markdown

Closes #8646

The Checkbox checkmark is a ::before on the input that wipes in and out with a clip-path animation on every toggle, and nothing in the component looked at prefers-reduced-motion, so the wipe played for everyone.

Whether the mark is shown is decided by visibility; the clip-path animation only adds motion on top of that. So I moved the decorative parts behind @media (prefers-reduced-motion: no-preference), which is how Skeleton (SkeletonBox.module.css, lines 18-24) and SegmentedControl (SegmentedControl.module.css, line 119) already opt in to motion, and gave the checked/indeterminate mark a static un-clipped clip-path.

In packages/react/src/Checkbox/Checkbox.module.css:

  • &::before keeps visibility: hidden and clip-path: inset(var(--base-size-16) 0 0 0); transition: visibility 0s linear 230ms and animation: checkmarkOut ... moved into the no-preference block.
  • &:checked::before, &:indeterminate::before now set clip-path: inset(0 0 0 0) unconditionally, and keep clip-path: inset(var(--base-size-16) 0 0 0) together with animation: checkmarkIn ... forwards 80ms inside the no-preference block.

The clipped start value has to stay next to the checkmarkIn animation instead of being replaced outright: that animation is forwards with an 80ms delay, so during the delay the mark is concealed by clip-path alone. Anyone without a preference therefore gets exactly the declarations it uses today, and the wipe is unchanged. With motion reduced, the mark is present when checked or indeterminate and hidden when unchecked, with no wipe and no 230ms hide delay.

The overrides are written on the &:checked::before / &:indeterminate::before compound selectors rather than on the base &::before, because .Checkbox:checked::before (0-2-1) beats .Checkbox::before (0-1-1) and an override on the base selector would not reach the checked state.

I left the other animated components alone. Spinner and the deprecated Tooltip aside, ActionBar and UnderlineNav animate with animation-timeline: scroll(), which is overflow detection doing real work rather than decoration, so prefers-reduced-motion should not apply there.

Changelog

New

  • Nothing.

Changed

  • Checkbox: the checkmark's clip-path animation and the delayed visibility transition now only apply when the user has no preference for reduced motion. No API changes.

Removed

  • Nothing.

Rollout strategy

  • No changes needed in github/github-ui
  • Needs changes in github/github-ui
  • Ships with feature flag(s)
  • Not sure
  • None

Testing & Reviewing

  • npx stylelint packages/react/src/Checkbox/Checkbox.module.css — clean.
  • npx vitest run packages/react/src/Checkbox/Checkbox.test.tsx — 16 passed.
  • npx eslint e2e/components/Checkbox.test.ts and npx tsc --noEmit (root config, which covers e2e/) — clean.
  • Two new Playwright tests in e2e/components/Checkbox.test.ts. toHaveCSS cannot target a pseudo-element, so they read getComputedStyle(input, '::before') on [data-component="Checkbox"]. Under reducedMotion: 'reduce' the checked and indeterminate marks report animation-name: none, clip-path: inset(0px), visibility: visible and a 0s transition delay, and clearing the mark hides it immediately. Under reducedMotion: 'no-preference' the same reads report the checkmarkIn / checkmarkOut animations and the 0.23s hide delay, so the animated behaviour is intact.
  • To check that the tests actually test something, I ran them against the CSS on main: the reduced-motion test fails there and the no-preference test passes, which is what you would want to see.
  • I also loaded the checkbox stories in Chromium against a local Storybook with prefers-reduced-motion: reduce emulated and screenshotted the checked, unchecked and indeterminate states. The marks render as they do today.

No screenshots are attached, because the change is invisible in a still image: the wipe is gone, the end state is not. I expect the existing @vrt snapshots to pass unchanged — CI captures them with contextOptions.reducedMotion: 'reduce' and animations: 'disabled', and under both the old and the new CSS the mark ends up fully un-clipped and visible. I could not run the snapshot comparisons locally, so if a Checkbox snapshot does move, that is the thing to look at first.

One note on the test tags: the new tests are tagged @vrt because the VRT and AAT shards are the only jobs that run Playwright here (--grep @vrt / --grep @aat), and an untagged test would never run. They assert computed styles rather than taking screenshots, following the forced-colors test in e2e/components/SelectPanel.test.ts.

The clip-path wipe on the checkmark is decorative - whether the mark is
shown is driven by visibility - so move the animation and the 230ms hide
delay behind @media (prefers-reduced-motion: no-preference), the same way
Skeleton and SegmentedControl opt in to motion. With motion reduced the
mark is simply present when checked or indeterminate.

The animated path keeps the declarations it uses today, including the
clipped start value that covers the 80ms delay on checkmarkIn, so the
rendered end state is unchanged.
@adityalenova
adityalenova requested a review from a team as a code owner October 31, 2026 10:33
@adityalenova
adityalenova requested review from siddharthkp and a balanced review from Copilot and removed request for Copilot October 31, 2026 10:33
@changeset-bot

changeset-bot Bot commented Oct 31, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 513d61d

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 1 package
Name Type
@primer/react Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

This branch was successfully deployed

1 active deployment
github-pages — 513d61d3 Deployed Oct 31, 2026 by adityalenova via deploy-preview #30289
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.

Checkbox: checkmark animation ignores prefers-reduced-motion

1 participant