Repository navigation
Conversation
Introduce a `label?: string` prop as the primary way to set the button text. The `children` prop keeps working as a deprecated fallback (when both are set, `label` wins) and emits a dev-only warning. This decouples the button layout from arbitrary child structures and makes `uppercase` work reliably, since the label is always a string.
Update the components that compose Button (Banner, Snackbar, DataTablePagination) to pass the new `label` prop instead of children, and update the `## Usage` / `@example` JSDoc blocks (and the test files) accordingly so nothing relies on the deprecated `children` prop.
Add an `iconPosition?: 'leading' | 'trailing'` prop to control where the
icon sits relative to the label. The previous approach of setting
`contentStyle={{ flexDirection: 'row-reverse' }}` still works but is now
deprecated and emits a dev-only warning.
The icon margins are extracted into a `getButtonIconStyle` helper,
replacing the previous matrix of computed StyleSheet keys, and
DataTablePagination is updated to use the new prop.
Add a `rippleColor?: ColorValue` prop and, by default, drive the ripple / state layer with the label color at the pressed-state opacity (per Material Design 3) instead of TouchableRipple's onSurface-based default. The color is computed by a new `getButtonRippleColor` helper, which falls back to `undefined` (TouchableRipple's own default) when the label color is not a plain string, e.g. an Android Material You PlatformColor.
Wrap the expensive derived values (color computation, border-radius extraction, ripple color, icon style, touchable ripple style, and the flattened style objects) in `useMemo`, memoize the press handlers with `useCallback`, and replace the `isMode` `useCallback` with a plain local function. No behavior or render-output change.
Update the example screens to use the new `label` prop instead of
children, and the `iconPosition="trailing"` prop instead of the
`contentStyle={{ flexDirection: 'row-reverse' }}` hack.
Update the hand-written guide snippets (icons, react-navigation, ripple effect) and the docs-site example components to use the new `label` prop instead of children. The generated component reference pages are derived from the JSDoc and will be regenerated by the docs build.
Add a `size?: 'extra-small' | 'small' | 'medium' | 'large' | 'extra-large'` prop. When omitted, the Button keeps its current visuals; when set, the per-size MD3 metrics (minHeight, horizontal padding, icon size, icon/label gap, label typescale) are applied via a new `getButtonSizeStyle` helper.
Add a `shape?: 'round' | 'square'` prop. When omitted, the button keeps its legacy corner radius. When set, `'round'` uses the full-pill radius and `'square'` uses a per-size smaller corner; the mapping comes from a new `getButtonShapeRadius` helper. An explicit `borderRadius` in `style` still wins.
Add a `selected?: boolean` prop. When `true`, the button flips its `shape` (round ↔ square) so the selected/unselected pair contrasts, and for `outlined`/`text` modes adopts a filled tonal-selected appearance (`secondaryContainer` background, `onSecondaryContainer` label, no border). `accessibilityState.selected` is set so screen readers announce the toggle state. Other modes keep their colors and only flip the shape. The `selected` flag is threaded through `getButtonColors` and its sub-helpers.
Showcase the new expressive props in the example app: one button per size in the Size section, a round and a square row across sizes in the Shape section, and stateful selected/unselected toggles in the Toggle section.
Treat `iconPosition` as logical (reading-order) and swap leading/trailing under RTL. The layout engine already mirrors `flexDirection` and physical margins when the platform direction is RTL (native I18nManager, or react-native-web with an active I18nManager), so we only flip manually when the locale direction disagrees with the platform default — e.g. a web LocaleProvider override where I18nManager is a no-op. This avoids a double-flip on native RTL.
Use a single `label` vocabulary internally to match the public `label` prop and the MD3 spec: - getButtonColors returns labelColor/labelOpacity (was textColor/textOpacity) - getButtonTextColor -> getButtonLabelColor; customTextColor -> customLabelColor - getButtonRippleColor takes labelColor Also drop the md3 prefix from the no-size styles (md3Label* -> legacyLabel*). The public `textColor` prop is unchanged. Internal-only rename; no behavior or snapshot change.
Align the outlined variant with the MD3 spec: - border color: theme.colors.outline (was outlineVariant) - label color: theme.colors.onSurfaceVariant (was primary) text and elevated modes keep the primary accent; the selected-outlined tonal branch is unchanged. Snapshot updates in Menu and DataTable reflect the same outlined Button color change.
For the legacy (no-`size`) button: - icon size 18 -> 20dp - horizontal padding 24 -> 16dp (legacyLabel marginHorizontal) - keep the 8dp icon-label gap: the icon's negative margin is calibrated against the label margin, so the non-text icon margins go -16 -> -8 to compensate for the smaller label margin (otherwise the gap collapses to 0). Snapshot updates in Menu and DataTable reflect the same legacy Button metrics.
Add src/components/Button/tokens.ts with one token object per MD3 expressive
size (modelled on Jetpack Compose's Button{Size}Tokens), replacing the inline
BUTTON_SIZE_STYLES and BUTTON_SHAPE_RADIUS maps. Corner radii now reference
shape keys (full / medium / large / extraLarge) resolved against
theme.shapes.corner instead of magic numbers.
- getButtonSizeStyle derives from the tokens
- getButtonShapeRadius takes theme and resolves corner keys (resolveButtonCorner)
- fix extra-small iconSize 16 -> 20 per MD3 spec
- tokens also carry pressed/selected shape fields for the upcoming shape morph
No metric/radius change other than the extra-small icon size.
Align Button mode names with the MD3 spec. This is a breaking rename (no aliases), matching the v6 breaking-change window: - contained -> filled - contained-tonal -> tonal The resolved styles are unchanged, so there is no visual difference. Updates the Button mode type, internal isMode checks, CardActions' injected default, and all consumers (example, docs, tests). Card/IconButton/SegmentedButtons/ ToggleButton keep their own independent contained modes.
Change the default Button mode from text to filled to match the MD3 emphasis hierarchy (filled is the highest-emphasis, primary button). A bare <Button> now renders filled instead of text. All library-internal usages pass an explicit mode, so none are affected. Snapshots updated for bare buttons (transparent -> primary background, primary -> onPrimary label).
Extra-small (32dp) and small (40dp) buttons are shorter than the 48dp minimum accessible touch target, so expand the press area with hitSlop without changing the visual size (XS -> top/bottom 8, S -> top/bottom 4). A user-supplied hitSlop wins on the axes it sets; a numeric hitSlop is respected as-is. Verified on device that taps inside the slop zone register and taps outside do not.
Shaped buttons animate their corner radius with the theme motion spring: to corner.small (8dp) while pressed, and between the round/square radii when the selected toggle flips the shape. Stability: the animated path resolves round to the real pill radius (minHeight/2) instead of the cornerFull sentinel so the spring stays bounded, plus a >= 0 clamp guards against overshoot. Scoped to shaped buttons that don't pin a radius via style; legacy/size-only buttons keep a static corner. Web: the inner ripple can't follow an Animated value, so it's rendered as a rectangle and the Surface clips it (overflow: hidden) to the morphing radius, keeping the outline and state layer in sync.
Replace the long exhaustive grid with a compact playground: a live Button driven by Chip controls (mode/size/shape/icon-position) and Switch rows (show icon/disabled/loading/selected/compact), with smart gating so a control is never a no-op (icon-position only with an icon; compact only when size is unset). Keeps trimmed showcase sections: Modes, States, Size, Shape, Toggle, Custom.
Update the Button doc data sources to match the v6 API: rename the contained/contained-tonal color + screenshot entries to filled/tonal, set the outlined label to onSurfaceVariant, and switch mode="contained" to mode="filled" in the react-navigation and ripple-effect guides. The Button.mdx page is generated from these sources plus the component JSDoc.
Resolve conflicts between the MD3 expressive Button work and main: - Button: adopt main's aria props (`aria-label`, `role`, `aria-disabled`) and add `aria-selected` for the toggle state; migrate off the removed `utils/forwardRef` to React 19 ref-as-prop, keeping the new `size`, `shape`, `selected`, `iconPosition`, `label` and `rippleColor` props. - Button/utils: drop the now-redundant `as Theme` assertions. - Button tests: migrate to main's RNTL 14 conventions (async `render`, `screen` queries, awaited `fireEvent`, `@jest/globals`) and the `tokens.md.sys.state.opacity` path; snapshots regenerated for the accessibility -> aria rename. - Docs: take main's rewritten GetStartedButtons, drop the docs test main removed, and re-apply the `label` migration to the 6.x icons guide (rename detection had landed it on the frozen 5.x copy).
lukewalczak
reviewed
Nov 6, 2026
lukewalczak
reviewed
Nov 6, 2026
lukewalczak
reviewed
Nov 6, 2026
lukewalczak
reviewed
Nov 6, 2026
lukewalczak
reviewed
Nov 6, 2026
lukewalczak
approved these changes
Nov 6, 2026
lukewalczak
left a comment
Member
There was a problem hiding this comment.
Thanks @oleksandrzavarzin-callstack for handling that and addressing all comments! 🙌🏽
Doberjohn
approved these changes
Nov 7, 2026
Doberjohn
left a comment
Collaborator
There was a problem hiding this comment.
All looks great approved and we can merge after one quick docs fix. The roundness section of the migration guide, merged in from #5183, still lists Button under largeIncreased, and its example is <Button theme={{ shapes: { corner: { largeIncreased: 10 } } }}>. Button no longer reads largeIncreased, so that example does nothing. Could you:
- drop
Buttonfrom thelargeIncreasedrow (it stays forSegmentedButtons) and add a line after the table saying thatButtonis a full pill by default and thatshape="square"reads the corner of its size, as in the Button section's Style notes, and - change the example to the square shape, e.g.
<Button shape="square" theme={{ shapes: { corner: { medium: 10 } } }}>(5 × 2 = 10 at the default size)?
oleksandrzavarzin-callstack
deleted the
feat/button-material-3-expressive
branch
November 9, 2026 08:24
Doberjohn
added a commit
that referenced
this pull request
Nov 9, 2026
…ns (#5196) Add two lines to the Button appearance changes in the 6.x migration guide: the outlined outline is drawn in outlineVariant instead of outline, also when disabled (5.x used surfaceDisabled), and Banner actions are text buttons instead of compact ones, with 16dp of horizontal padding insteadof 8dp and a 64dp minimum width. Follow-up to #5097.
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.
Motivation
Buttonhad one size, no shape scale, no toggle, MD2 mode names and RNAnimatedanimations. This PR brings it to Material Design 3 Expressive:size,shape, aselectedtoggle and a corner morph on press. It takes over #4943.Related issue
Part of #4928
Screenshots / Videos
Android | iOS
Screen.Recording.2026-09-03.at.15.45.58.2.mp4
Test plan
large, then press and hold Play me. The corner tightens with a small overshoot.selected. The shape flips between round and square.outlinedwithselectedon, then turn on Disabled. The button keeps a light fill and has no outline.aria-pressedon web andaria-checkedon native.BREAKING CHANGE:
Buttonmoves to Material Design 3 Expressive, with renamed modes, afilleddefault and removed props, and the migration guide lists each change.