Skip to content

feat(button): sync the md theme with Material Design 2 - #31636

Open
thetaPC wants to merge 27 commits into
FW-6838-snapshotsfrom
FW-6838-md-sync
Open

thetaPC wants to merge 27 commits into
FW-6838-snapshotsfrom
FW-6838-md-sync

Conversation

@thetaPC

@thetaPC thetaPC commented Oct 22, 2026 •

Copy link
Copy Markdown
Contributor

Issue number: resolves internal


What is the current behavior?

md buttons use values carried over from v9 instead of Material Design 2: 15.4px padding, 0.06em tracking, a 2px outline in the button's color, a 0.5 fade when disabled, and the same elevation on hover and focus as at rest.

What is the new behavior?

md buttons follow Material Design 2, using the values from Material Components Web 14, Google's MD2 implementation:

  • 16px padding and a 64px min width at the default size, and 0.0893em letter spacing
  • 18px icons with an 8px gap to the label, scaled at the small and large sizes
  • A 1px outline in on-surface at 12%, for colored outline buttons too
  • Elevation 4 on hover and focus for solid buttons
  • Disabled buttons use 38% on-surface text and a 12% container with no elevation, instead of fading the whole button. Material shows disabled elements in a neutral color rather than their own, so a disabled button no longer keeps its color (Material Design 2: disabled state)
  • The pressed ripple is 24% on solid buttons and 12% on outline and clear
  • Spinners match the 18px icon size, so a button no longer grows to fit one. MD2 allows a spinner in a button but doesn't size it there, so it follows the icon. Other spinner sizes keep their usual ratio to it

To support this, the recipe gains hover.box.shadow, focus.box.shadow and disabled.{color, border.color, box.shadow}. A theme that doesn't set the disabled colors keeps its default colors, so ios and ionic don't change.

Toolbar and datetime buttons keep their current look: no elevation on hover or focus, and disabled toolbar buttons still fade their own colors.

Snapshots are in #31637.

Does this introduce a breaking change?

  • Yes
  • No

The md button follows Material Design 2 for its spacing, outline, elevation and disabled state. To keep a previous value, set the token for global styles, or the matching --ion-button-* CSS variable for component-specific styles. Replace <fill> with solid, outline or clear, and <size> with small, medium or large.

Property Previous New Token
Padding at the default size 1.1em 16px IonButton.size.medium.padding.start and .end
Min width at the default size none 64px IonButton.size.medium.min.width
Letter spacing 0.06em 0.0893em IonButton.letter.spacing
Icon size 1.35em 1.2857em (18px at the default size) IonButton.size.<size>.icon.font.size
Gap between icon and label 0.3em 0.4444em (8px at the default size) IonButton.size.<size>.icon.leading.margin.end and .trailing.margin.start
Spinner in a button the spinner's own size (28px by default) 1.2857em by default (18px at the default size), with the other spinner sizes in their usual ratio to it IonButton.size.<size>.spinner.size.<spinner size>.width and .height
Outline border 2px, the button's color 1px, the text color at 12% IonButton.hue.bold.outline.default.border.width and .color (.semantic.default.border.color with a color)
Solid elevation on hover and focus the resting elevation elevation 4 IonButton.hue.bold.solid.hover.box.shadow and .focus.box.shadow
Disabled the whole button at 0.5 opacity 38% text color, a 12% container and no elevation IonButton.hue.bold.<fill>.disabled.opacity, .color, .background and .box.shadow
Pressed ripple 16% 24% on solid, 12% on outline and clear IonButton.hue.bold.<fill>.rippleEffect.opacity

Buttons in toolbars keep their previous colors when disabled and have no elevation on hover or focus.

Other information

Not matched to MD2 on purpose

Known gaps

  • The a11y spec still skips a pressed outline button in md's dark toolbar (TODO(FW-6417)). The lighter ripple helps, but the text still falls short of WCAG's 4.5:1. The fix belongs to the dark palette

How to test

Check Before After What to look for
Spacing and icons Before After Labels are spaced wider, icons are slightly smaller with more room before the label
Outline and disabled Before After Borders are thin and gray. Disabled buttons are gray instead of faded
Hover and focus Before After Hover over or tab to a solid button: its shadow grows
Toolbars Before After Only the letter spacing changes, and colored outline buttons get the thin gray border

@thetaPC
thetaPC added this pull request to stack #31629 October 22, 2026 22:56
@vercel

vercel Bot commented Oct 22, 2026 •

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated
ionic-framework Ready Ready Preview Nov 4, 2026 7:56pm UTC

Request Review

@thetaPC
thetaPC marked this pull request as ready for review October 29, 2026 16:00
@thetaPC
thetaPC requested a review from a team as a code owner October 29, 2026 16:00
@thetaPC
thetaPC requested a review from brandyscarney October 29, 2026 16:00

@ShaneK ShaneK left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Looks really good, every md value I checked lines up with the PR description! Just a few things, mainly the readonly datetime buttons going gray and the toolbar icon buttons growing to 64px.

background: 'transparent',

opacity: '0.5',
color: rgba('var(--ion-text-color-rgb, 0, 0, 0)', 0.38),

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

A readonly datetime disables its Cancel, Clear and Done buttons and sets opacity: 1 so they don't look disabled, but now that md shows disabled through the color they come out at rgba(0, 0, 0, 0.38) instead of primary. Could the readonly rule reset the disabled color too? The same thing happens in ios in #31645, so a fix in datetime would cover both.

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.

Good catch. The readonly rule now sets each disabled color to the matching enabled one, so the action buttons keep their colors. It lives in the datetime, so it covers ios in #31645 as well. Done in 8627a1c.

Comment thread BREAKING.md Outdated
| Disabled | the whole button at `0.5` opacity | `38%` text color, a `12%` container and no elevation | `IonButton.hue.bold.<fill>.disabled.opacity`, `.color`, `.background` and `.box.shadow` |
| Pressed ripple | `16%` | `24%` on solid, `12%` on outline and clear | `IonButton.hue.bold.<fill>.rippleEffect.opacity` |

Buttons in toolbars keep their previous colors when disabled and have no elevation on hover or focus.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

This reads like every toolbar button loses the hover and focus elevation, but the none overrides are only under IonToolbar.IonButtons. A solid button placed straight in ion-toolbar still goes from elevation 2 to elevation 4 when focused. Should this say buttons in ion-buttons, or should the overrides move up to IonToolbar.IonButton?

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.

A solid button placed directly in a toolbar keeps the regular solid elevation, the same as on next, so I limited the sentence to ion-buttons instead of moving the overrides. Done in a4c5c8e.

};
};

/** Unset colors keep the default ones */

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Only color and border.color fall back to the default here, background doesn't. Maybe something like "An unset color or border.color keeps the default one"?

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.

Done in 853c5f3.

},
},

spinner: {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

The comment on the spinner rules in button.scss still says only the ionic theme sizes and spaces a slotted spinner, but md sizes it now too. Could we update it to something like "The ionic and md themes size a slotted spinner, and only ionic spaces it"?

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.

Updated in 853c5f3. I left the themes out so it doesn't go stale again: "A theme can size a slotted spinner for each button size. Without those tokens, the spinner keeps its own size."

Comment thread BREAKING.md Outdated
| Spinner in a button | the spinner's own size (`28px` by default) | `1.2857em` by default (`18px` at the default size), with the other spinner sizes in their usual ratio to it | `IonButton.size.<size>.spinner.size.<spinner size>.width` and `.height` |
| Outline border | `2px`, the button's color | `1px`, the text color at `12%` | `IonButton.hue.bold.outline.default.border.width` and `.color` (`.semantic.default.border.color` with a `color`) |
| Solid elevation on hover and focus | the resting elevation | elevation 4 | `IonButton.hue.bold.solid.hover.box.shadow` and `.focus.box.shadow` |
| Disabled | the whole button at `0.5` opacity | `38%` text color, a `12%` container and no elevation | `IonButton.hue.bold.<fill>.disabled.opacity`, `.color`, `.background` and `.box.shadow` |

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

The outline row mentions the .semantic path for buttons with a color, but this row and the pressed ripple row don't, and both changed under semantic too. Could they mention it as well?

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.

Added the .semantic paths to both rows in f3a7c79.

medium: {
min: {
height: 'var(--ion-scaling-900)',
width: 'var(--ion-scaling-xxxxl)',

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

With this min width, a toolbar icon button whose icon isn't in slot="icon-only" grows from 35px to 64px, while one with slot="icon-only" stays at 48px. You can see it in the nested popover snapshots in #31637, where the popover shifts too. Leaving out the slot is pretty common in apps, so could IonToolbar.IonButtons.IonButton reset the min width, or should this go in BREAKING.md?

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.

I went with the reset, but in button.scss so every theme gets it, custom ones included: a button in ion-buttons skips its size's min width. Icon buttons without slot="icon-only" are back to their previous width, and text buttons fit their content. In ionic, this also drops the 72px min width from toolbar buttons with short labels, which matches the Top Bar design. Done in 1a05f91.

…md-sync

# Conflicts:
#	core/src/themes/md/default.tokens.ts

This branch was successfully deployed

1 active deployment
Preview — 1a05f917 Deployed Nov 4, 2026 by vercel[bot]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

package: core @ionic/core package

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants