Skip to content

feat(checkbox): modernize for md3 spec compliance#4962

Open
fabriziocucci wants to merge 5 commits into
callstack:mainfrom
fabriziocucci:feat/checkbox-md3-modernization
Open

feat(checkbox): modernize for md3 spec compliance#4962
fabriziocucci wants to merge 5 commits into
callstack:mainfrom
fabriziocucci:feat/checkbox-md3-modernization

Conversation

@fabriziocucci
Copy link
Copy Markdown
Contributor

@fabriziocucci fabriziocucci commented May 22, 2026

Summary

Stacked on #4952 (token reorg + useFocusVisible hook).

Reviewers: because #4952 hasn't merged yet, the diff against main here also includes the 4 foundation commits from that PR (token reorg, shadow overrides, useFocusVisible, motion mass). The actual Checkbox change is just the final commit: 9ec676c. When #4952 lands, this PR rebases and the diff shrinks accordingly.

Bring the Checkbox component up to the Material Design 3 spec, end-to-end:

  • 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 → primary, unselected → onSurface, pressed-selected 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 from feat: improve structure of ref and sys tokens; add useFocusVisible hook #4952.
  • Animations approximate androidx.compose.material3.Checkbox.kt: 100ms fill transition + 150ms checkmark draw. The checkmark is sequenced short-leg then long-leg via scaleY to suggest the stroke fraction. Indeterminate uses a scaleX-animated dash.

Why not SVG

Compose Material3 draws the checkmark as a Path with strokeFraction 0 → 1. Doing the same in RN would mean adding react-native-svg as 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 (Pressable 40dp tap target, state-layer overlay, animated 18dp container, focus ring, view-based checkmark + dash).
  • src/components/Checkbox/utils.ts: new getSelectionVisualState returns the full color + opacity + outline-width picture for any (selected × hovered × focused × pressed × disabled × error × customColor) combination. Legacy getSelectionControlColor kept as a back-compat export for RadioButtonAndroid.
  • 9 snapshots auto-updated to reflect the new render tree.

Out of scope

Test plan

  • yarn typescript clean
  • yarn lint clean
  • yarn jest: 736/737 (1 skipped, pre-existing); 159/159 snapshots pass (9 updated for the new renderer).

Visuals

Android iOS
checkbox-md3-android checkbox-md3-ios

@callstack-bot
Copy link
Copy Markdown

callstack-bot commented May 22, 2026

Hey @fabriziocucci, thank you for your pull request 🤗. The documentation from this branch can be viewed here.

@fabriziocucci fabriziocucci changed the base branch from main to @adrcotfas/tokens_structure May 22, 2026 09:35
@fabriziocucci fabriziocucci changed the base branch from @adrcotfas/tokens_structure to main May 22, 2026 09:36
@fabriziocucci fabriziocucci force-pushed the feat/checkbox-md3-modernization branch 2 times, most recently from 90c056e to 9ec676c Compare May 22, 2026 12:19
@fabriziocucci fabriziocucci marked this pull request as ready for review May 22, 2026 12:37
Copilot AI review requested due to automatic review settings May 22, 2026 12:38
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Modernizes the Checkbox component to align with Material Design 3 interaction/visual specs, and (because this PR is stacked on #4952) includes foundational theme token reshaping plus new shared utilities (useFocusVisible, getStateLayer, token tree updates) that support MD3-compliant state layers and motion.

Changes:

  • Reworks MD tokens into md.ref.* (raw values) and md.sys.* (semantic/system decisions), adding md.sys.state.* and updating scheme builders/utilities accordingly.
  • Adds useFocusVisible and a shared getStateLayer(theme, role, state) helper; migrates several components/utilities to the new state-layer token source.
  • Rewrites Checkbox rendering/animations to MD3: 40dp tap target + state layer overlay + focus ring + new checkmark/dash rendering, and updates related snapshots.

Reviewed changes

Copilot reviewed 43 out of 43 changed files in this pull request and generated 4 comments.

Show a summary per file
File Description
src/utils/useFocusVisible.ts New hook to gate focus indicators on :focus-visible semantics.
src/types.tsx Re-exports theme types and centralizes shared utility TS types.
src/theme/utils/state.ts New helper for resolving {color, opacity} state layers from tokens + theme.
src/theme/types/utils.ts Removes old theme-scoped utility type exports (moved to src/types.tsx).
src/theme/types/state.ts Adds StateLayer / StateOpacityKey types for state-layer utilities.
src/theme/types/motion.ts Adds RawSpring type used by motion helpers.
src/theme/types/index.ts Exports new state types and drops utils export.
src/theme/types/color.ts Adds ColorRole type to constrain valid color-role keys.
src/theme/tokens/sys/typography.ts Re-homes typescale under md.sys.typescale and references new typeface tokens.
src/theme/tokens/sys/state.ts Adds MD3 interaction state tokens (opacity + focus indicator sizing).
src/theme/tokens/sys/motion.ts Updates toRawSpring to include MD3 mass + documents reanimated usage.
src/theme/tokens/sys/elevation.ts Adds overload typings for animated vs static shadow styles.
src/theme/tokens/sys/color.ts Updates scheme builder to use sys state tokens and ref palette directly.
src/theme/tokens/ref/typeface.ts Adds ref typeface tokens (platform font families + weights).
src/theme/tokens/ref/palette.ts Adds ref tonal palette tokens as a standalone module.
src/theme/tokens/index.ts Rebuilds the token tree into md.ref + md.sys and re-exports palette/typescale.
src/theme/schemes/LightTheme.tsx Updates scheme building call signature after token refactor.
src/theme/schemes/DarkTheme.tsx Updates scheme building call signature after token refactor.
src/components/TextInput/helpers.tsx Switches to md.sys.state.opacity for disabled/enabled opacity.
src/components/TextInput/Adornment/utils.ts Uses new getStateLayer and sys state opacities for adornment colors.
src/components/SegmentedButtons/utils.ts Switches to md.sys.state.opacity.
src/components/RadioButton/utils.ts Switches to md.sys.state.opacity.
src/components/RadioButton/RadioButtonItem.tsx Uses getStateLayer for label color/opacity.
src/components/Modal.tsx Switches scrim alpha to md.sys.scrim.alpha.
src/components/Menu/utils.ts Switches to md.sys.state.opacity.
src/components/IconButton/utils.ts Switches to md.sys.state.opacity.
src/components/HelperText/utils.ts Uses getStateLayer for error/disabled/info text colors.
src/components/Chip/helpers.tsx Switches to md.sys.state.opacity.
src/components/Checkbox/utils.ts Adds getSelectionVisualState for MD3 checkbox visuals; keeps legacy helper for radio.
src/components/Checkbox/CheckboxItem.tsx Uses getStateLayer for label styling (opacity + color).
src/components/Checkbox/Checkbox.tsx Full rewrite: Pressable tap target, state layer overlay, focus ring, new animations/glyph rendering.
src/components/Button/utils.tsx Switches to md.sys.state.opacity.
src/components/tests/TextInput.test.tsx Updates tests to read opacities from md.sys.state.opacity.
src/components/tests/SegmentedButton.test.tsx Updates tests to read opacities from md.sys.state.opacity.
src/components/tests/RadioButton/utils.test.tsx Updates tests to read opacities from md.sys.state.opacity.
src/components/tests/Modal.test.tsx Updates tests to read scrim alpha from md.sys.scrim.alpha.
src/components/tests/MenuItem.test.tsx Updates tests to read opacities from md.sys.state.opacity.
src/components/tests/IconButton.test.tsx Updates tests to read opacities from md.sys.state.opacity.
src/components/tests/Chip.test.tsx Updates tests to read opacities from md.sys.state.opacity.
src/components/tests/Checkbox/utils.test.tsx Updates tests to read opacities from md.sys.state.opacity.
src/components/tests/Button.test.tsx Updates tests to read opacities from md.sys.state.opacity.
src/components/tests/Checkbox/snapshots/CheckboxItem.test.tsx.snap Snapshot updates for the new Checkbox render tree.
src/components/tests/Checkbox/snapshots/Checkbox.test.tsx.snap Snapshot updates for the new Checkbox render tree.
Comments suppressed due to low confidence (1)

src/components/Checkbox/Checkbox.tsx:21

  • Checkbox previously inherited TouchableRipple props (via $RemoveChildren<typeof TouchableRipple>). Redefining Props as a small custom object and not forwarding any other props to Pressable is a breaking public API change (e.g., style, hitSlop, accessibilityLabel, onLongPress, testID-adjacent props, etc.). Consider extending Pressable/TouchableRipple props again and forwarding ...rest to the root element to preserve compatibility.
export type Props = {
  /**
   * Status of checkbox.
   */
  status: 'checked' | 'unchecked' | 'indeterminate';

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +6 to +8
type FocusVisibleEvent = {
currentTarget: object;
};
Copy link
Copy Markdown
Contributor Author

@fabriziocucci fabriziocucci May 22, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This file was introduced by the foundation PR #4952 that this branch is stacked on. The type issue will be resolved when #4952 merges (or in a follow-up on that PR). Leaving this file untouched in the Checkbox PR to avoid modifying the shared foundation.

Comment thread src/components/Checkbox/Checkbox.tsx
Comment thread src/components/Checkbox/utils.ts
Comment on lines 1 to 6
import { getTheme } from '../../../core/theming';
import { tokens } from '../../../theme/tokens';
import { getSelectionControlColor } from '../../Checkbox/utils';
const { stateOpacity } = tokens.md.ref;

const stateOpacity = tokens.md.sys.state.opacity;

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The helper is covered by the snapshot tests in Checkbox.test.tsx which exercise the full state matrix (unchecked/checked/indeterminate × disabled/error × hover/focus/pressed). The snapshots assert the exact RGBA values produced for each combination, so the helper's decision logic is verified. Dedicated unit tests for the helper would be valuable but aren't blocking for this PR — the snapshots already guard against regressions. Can add if you prefer.

@fabriziocucci fabriziocucci force-pushed the feat/checkbox-md3-modernization branch from 9ec676c to 1ce4264 Compare May 22, 2026 13:13
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.
@fabriziocucci fabriziocucci force-pushed the feat/checkbox-md3-modernization branch from 1ce4264 to 414277c Compare May 22, 2026 13:13
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.

4 participants