feat(checkbox): modernize for md3 spec compliance#4966
Open
fabriziocucci wants to merge 1 commit into
Open
Conversation
Rewrites the Checkbox renderer to match the Material Design 3 spec (https://m3.material.io/components/checkbox/specs): - 18dp container with 2dp outline (unselected) / 0dp outline + theme primary fill (selected), inside a 40dp state-layer tap target. - State-layer overlay renders hover (8%), focus (10%) and pressed (10%) layers in the color the spec defines for each (selected pressed flips to onSurface; error always wins). - Focus indicator: 3dp ring at theme.colors.secondary with the 2dp outer-offset from md.sys.state.focusIndicator. Gated on :focus-visible via the useFocusVisible hook added in callstack#4952. - Animations approximate Compose Material3 Checkbox.kt: 100ms fill transition and 150ms checkmark draw, sequenced short-leg then long-leg to suggest the stroke fraction. Indeterminate uses a scaleX-animated dash. - No new peer-deps: the checkmark is built from two rotated rectangles (View-based), not an SVG path. utils.ts: - New getSelectionVisualState helper returns the full color + opacity + outline-width picture for a given state combo. - Legacy getSelectionControlColor kept as a compatibility export for RadioButtonAndroid (radio button modernization is out of scope for this PR). 9 snapshots auto-updated to reflect the new render tree.
cee7317 to
2288d5d
Compare
There was a problem hiding this comment.
Pull request overview
Note
Copilot was unable to run its full agentic suite in this review.
This PR modernizes the Checkbox implementation to better align with the Material Design 3 checkbox spec, including updated visuals/animations and corrected accessibility behavior for the indeterminate state.
Changes:
- Reworked
Checkboxrendering to MD3-style layers (state layer, focus ring, outline/fill, glyph reveal mask) and moved away from icon-font rendering. - Introduced centralized visual-state resolution via
getSelectionVisualStateand updated color/state-layer logic. - Updated tests and snapshots, including ARIA-compliant
checked: "mixed"handling for indeterminate checkboxes.
Reviewed changes
Copilot reviewed 5 out of 5 changed files in this pull request and generated 2 comments.
Show a summary per file
| File | Description |
|---|---|
| src/components/Checkbox/Checkbox.tsx | Reimplements Checkbox visuals/interaction using Pressable, MD3 tokens, and new animation/state-layer logic. |
| src/components/Checkbox/utils.ts | Adds getSelectionVisualState to compute MD3 visual state (colors/opacities) and keeps legacy helper for RadioButton. |
| src/components/tests/Checkbox/CheckboxItem.test.tsx | Updates accessibility-state assertions for indeterminate (“mixed” vs false). |
| src/components/tests/Checkbox/snapshots/Checkbox.test.tsx.snap | Updates snapshots to match new Checkbox structure and accessibilityState output. |
| src/components/tests/Checkbox/snapshots/CheckboxItem.test.tsx.snap | Updates snapshots to match new CheckboxItem structure and accessibilityState output. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Comment on lines
+152
to
169
| <Pressable | ||
| onPress={onPress} | ||
| onFocus={onFocus} | ||
| onBlur={onBlur} | ||
| onHoverIn={() => setHovered(true)} | ||
| onHoverOut={() => setHovered(false)} | ||
| onPressIn={() => setPressed(true)} | ||
| onPressOut={() => setPressed(false)} | ||
| disabled={disabled} | ||
| accessibilityRole="checkbox" | ||
| accessibilityState={{ disabled, checked }} | ||
| accessibilityState={{ | ||
| disabled, | ||
| checked: status === 'indeterminate' ? 'mixed' : status === 'checked', | ||
| }} | ||
| accessibilityLiveRegion="polite" | ||
| style={styles.container} | ||
| testID={testID} | ||
| theme={theme} | ||
| style={styles.tapTarget} | ||
| > |
| accessibilityRole="checkbox" | ||
| accessibilityState={{ disabled, checked }} | ||
| accessibilityState={{ | ||
| disabled, |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Stacked on #4952 (token reorg +
useFocusVisiblehook).Bring the
Checkboxcomponent up to the Material Design 3 spec, end-to-end:theme.colors.secondarywith the 2dp outer-offset frommd.sys.state.focusIndicator. Gated on:focus-visiblevia theuseFocusVisiblehook from refactor: improve structure of reference and system tokens #4952.androidx.compose.material3.Checkbox.kt: 100ms fill transition + 150ms checkmark draw. The checkmark is sequenced short-leg then long-leg viascaleYto suggest the stroke fraction. Indeterminate uses ascaleX-animated dash.Why not SVG
Compose Material3 draws the checkmark as a
PathwithstrokeFraction0 → 1. Doing the same in RN would mean addingreact-native-svgas a Paper peer-dep, an ecosystem-level change. This PR uses a reveal-mask instead: a static L-shape (borderLeftWidth + borderBottomWidth rotated -45°) inside a left-anchored View whose width animates 0 → 18dp with a matching opacity fade. The visual suggests the stroke drawing left-to-right without the SVG dependency. Same precedent will apply to RadioButton when it's modernized. If you'd prefer the SVG-based approach for v6, happy to switch. Picking once now sets the precedent.Files
src/components/Checkbox/Checkbox.tsx: full rewrite (Pressable40dp tap target, state-layer overlay, animated 18dp container, focus ring, view-based checkmark + dash).src/components/Checkbox/utils.ts: newgetSelectionVisualStatereturns the full color + opacity + outline-width picture for any(selected × hovered × focused × pressed × disabled × error × customColor)combination. LegacygetSelectionControlColorkept as a back-compat export forRadioButtonAndroid.Out of scope
Animatedtoreact-native-reanimated.<StateLayer />as a shared primitive (can follow once Radio + Switch land and the pattern is proven).Test plan
yarn typescriptcleanyarn lintcleanyarn jest: 736/737 (1 skipped, pre-existing); 159/159 snapshots pass (9 updated for the new renderer).Visuals