Skip to content

feat(button): add recipe and tokens - #31628

Draft
thetaPC wants to merge 4 commits into
FW-6838-testsfrom
FW-6838
Draft

thetaPC wants to merge 4 commits into
FW-6838-testsfrom
FW-6838

Conversation

@thetaPC

@thetaPC thetaPC commented Oct 21, 2026 •

Copy link
Copy Markdown
Contributor

Issue number: resolves internal


What is the current behavior?

ion-button fragments its styles across per-theme stylesheets (.ios, .md, .ionic, .native).

What is the new behavior?

  • Migrates ion-button to a single token-driven button.scss, with IonButtonRecipe / IonButtonConfig and per-theme tokens
  • Draws focus, hover and activated on the native element from the tokens instead of an ::after overlay
  • Makes icon-only buttons squares as tall as a text button of the same size (FW-7854)
  • Syncs iOS heights and padding to the iOS 17 kit (28/34/50px), and iOS clear buttons now use the same font size as the other fills
  • Moves disabled opacity to each fill. ionic uses 0.4, which matches its design's white wash
  • ionic outline buttons use the darker foreground for their label and the page background for their fill, matching the design
  • ionic toolbar buttons are 40px tall, matching the design's top bar
  • Sizes a slotted spinner per spinner size, so a size set on the spinner still applies
  • Moves parent color and size rules to nested tokens (IonToolbar.IonButtons.IonButton, IonDatetime.IonButton, IonInputPasswordToggle.IonButton)
  • Deprecates the default size in favor of medium

Snapshots are in #31630.

Does this introduce a breaking change?

  • Yes
  • No

The button's CSS variables are replaced by token-driven custom properties. Use the global token for theme-wide styling, or the CSS variable for component-specific overrides.

  1. The ios button's border-radius is 6px at every size, and md defaults to 999px instead of 4px. To revert, set shape="soft" for md. For ios, set IonButton.shape.soft.border.radius or --ion-button-shape-soft-border-radius to 14px (and 6px / 16px on small / large).
  2. Icon-only buttons in the start and end slots of ion-input, ion-select and ion-textarea follow the button's shape instead of always being circles. For ios, set shape="round" to keep them circular.
  3. ios clear buttons use the same font size as the other fills, instead of 17px at the default size. To keep it, set --ion-button-size-medium-font-size to 17px.
  4. The ios button is 28px, 34px and 50px tall at the small, default and large sizes, with less padding. To revert, set IonButton.size.<size>.min.height or --ion-button-size-<size>-min-height to 2.1em, 3.1em and 3.1em, and the padding to 4px 0.9em, 13px 1em and 17px 1em.
  5. Icon-only buttons are squares as tall as a text button of the same size. In md, they're a few pixels smaller at the small and default sizes and larger at the large size.
  6. Replaced CSS variables. Replace <fill> with solid, outline or clear, and <shape> with crisp, soft, round or rectangular. Buttons with a color read the same tokens under semantic.
Old (9.x) New token (global) New CSS variable (component-specific)
--background IonButton.hue.bold.<fill>.default.background --ion-button-hue-bold-<fill>-default-background
--background-activated IonButton.hue.bold.<fill>.activated.background --ion-button-hue-bold-<fill>-activated-background
--background-focused IonButton.hue.bold.<fill>.focus.background --ion-button-hue-bold-<fill>-focus-background
--background-hover IonButton.hue.bold.<fill>.hover.background --ion-button-hue-bold-<fill>-hover-background
--border-color IonButton.hue.bold.<fill>.default.border.color --ion-button-hue-bold-<fill>-default-border-color
--border-radius IonButton.shape.<shape>.border.radius --ion-button-shape-<shape>-border-radius
--border-style IonButton.hue.bold.<fill>.default.border.style --ion-button-hue-bold-<fill>-default-border-style
--border-width IonButton.hue.bold.<fill>.default.border.width --ion-button-hue-bold-<fill>-default-border-width
--box-shadow IonButton.hue.bold.<fill>.default.box.shadow --ion-button-hue-bold-<fill>-default-box-shadow
--color IonButton.hue.bold.<fill>.default.color --ion-button-hue-bold-<fill>-default-color
--color-activated IonButton.hue.bold.<fill>.activated.color --ion-button-hue-bold-<fill>-activated-color
--color-focused IonButton.hue.bold.<fill>.focus.color --ion-button-hue-bold-<fill>-focus-color
--color-hover IonButton.hue.bold.<fill>.hover.color --ion-button-hue-bold-<fill>-hover-color
--ripple-color IonButton.hue.bold.<fill>.rippleEffect.color --ion-button-hue-bold-<fill>-ripple-effect-color
--transition IonButton.transition --ion-button-transition

--background-activated-opacity, --background-focused-opacity and --background-hover-opacity have no one-to-one replacement: set the state's final color instead. --opacity is removed: set opacity on ion-button.

  1. --padding-* variables are no longer documented, but still work when set on the button itself. Replace <size> with small, medium or large:
Old (9.x) New token (global) New CSS variable (component-specific)
--padding-top IonButton.size.<size>.padding.top --ion-button-size-<size>-padding-top
--padding-end IonButton.size.<size>.padding.end --ion-button-size-<size>-padding-end
--padding-bottom IonButton.size.<size>.padding.bottom --ion-button-size-<size>-padding-bottom
--padding-start IonButton.size.<size>.padding.start --ion-button-size-<size>-padding-start
  1. Renamed classes:
Old (9.x) New
.button-solid / .button-outline / .button-clear .button-fill-solid / .button-fill-outline / .button-fill-clear
.button-small / .button-default / .button-large .button-size-small / .button-size-medium / .button-size-large
.button-soft / .button-round / .button-rectangular .button-shape-soft / .button-shape-round / .button-shape-rectangular
.button-block / .button-full .button-expand-block / .button-expand-full
  1. The default size is deprecated. It logs a warning and renders as medium.
  2. Theme classes: remove any styles that target ion-button.md or ion-button.ios.

Other information

Why these choices

  • Icon-only buttons use the min height for their width too: the iOS 17 kit, the Material 3 kit and the ionic design all give text and icon-only buttons the same height at each size, which is what FW-7854 asked for. One size.<size>.min.height token sets both, so a square can't drift from the text buttons beside it. Not aspect-ratio: it needs Firefox 89, and Ionic supports Firefox 75. Equal min-width and min-height make the same square in every supported browser.
  • iOS heights change here: iOS icon-only buttons were already 28, 34 and 50px on next, while text buttons were about 27, 50 and 62px. Sharing one height means picking one, and the kit's values match the icon-only buttons, so no iOS icon-only button changes size.
  • States set the native element's colors directly: the ::after overlay painted a state color at an opacity, which a theme couldn't set as one value. Each state's token is now the final color, rendering the same color as next (md's focused solid is still white at 24% over primary). Where next tinted with currentColor or shade, which have no -rgb variable, the closest variable stands in, a few RGB levels off; a comment marks each one. No color-mix(), since it needs Chrome 111, Safari 16.2 and Firefox 113: tints use rgba() or the new overlay() helper, which paints the same pixels.
  • md's ripple: the ripple is md's pressed feedback, so .button-native:has(ion-ripple-effect) keeps the normal background while activated. Browsers without :has() (before Chrome 105 or Firefox 121) show the activated background under the ripple, a double cue rather than a broken button.
  • Disabled opacity per fill: it sits next to each fill's hover and activated tokens. ionic uses 0.4 because its design draws a 60% white wash over the whole button, and 0.4 paints the same colors on a white page. Before, ionic stacked the wash and a 0.5 fade, so disabled buttons were fainter than the design.
  • Themes only set what differs: an unset token makes the property unset, which is its initial value, so no theme needs border: none, box-shadow: none or opacity: 1 entries.
  • --padding-* still works on the button: the button reads var(--padding-*, <token>) and resets the hooks on :host, so .ion-padding on the button applies, but an ancestor's (like ion-content.ion-padding) doesn't leak in. That matches ion-content and v9.
  • Spinner sizes per spinner size: one width and height would override a size set on the spinner. The scale starts at the theme's default spinner size, and ios and md set none, so they fall back to the spinner's own size as in v9.
  • Parent overrides are nested tokens: a child never knows its parents. IonToolbar: { IonButtons: { IonButton: { … } } } generates ion-toolbar ion-buttons ion-button { --ion-button-*: … }, and specificity settles overlaps (IonToolbar.IonButtons.IonButton outranks IonToolbar.IonButton). Datetime and the password toggle render their buttons inside their own shadow root, where page CSS can't reach, so their hosts pass the tokens down by inheritance. A spec fails if a component renders a migrated child without that entry.
  • iOS clear font size: the 17px came from the iOS 13 list header spec (feat(list-header): redesign list header for iOS spec #19915). The iOS 17 and 18 kits use the same font for plain and filled buttons.
  • @prop docs in _button.docs.scss: keeps button.scss starting with its rules. Stencil reads @prop comments from the compiled CSS, and the docs output (core.json, api.txt, html.html-data.json) lists the same variables as before.

Not matched to the design on purpose

Left for the parents

  • ion-buttons, datetime, input, select, textarea, item and list header still set --padding-* on buttons; each moves it to a nested token in its own migration
  • In ios and md toolbars, ion-buttons caps buttons at 32px tall, so the iOS toolbar icon button is 38×32

Known gaps

  • The a11y spec skips a pressed outline button in md's dark toolbar (TODO(FW-6838)). It fails contrast on next too
  • In ionic's dark snapshots, outline buttons show a white background: the test pages pin --ion-background-color to white. In an app, it follows the page

How to test

Check Before After What to look for
iOS sizes Before After Buttons are shorter, with less space above and below the label
Icon-only Before After Icon-only buttons are squares as tall as the text buttons beside them
ionic outline Before After Labels are a darker blue. Disabled buttons are fainter, the same amount as the other fills
ionic toolbar Before After Toolbar buttons are 40px tall, and icon buttons are 40×40 squares
md states Before After Hover, tab to, then press and hold each button. The tint shows on hover and focus, and the ripple on press
ionic spinner Before After The spinner in large buttons is bigger (24px)

@thetaPC
thetaPC added this pull request to stack #31629 October 21, 2026 23:00
@vercel

vercel Bot commented Oct 21, 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 Oct 22, 2026 6:32pm UTC

Request Review

@github-actions github-actions Bot added package: core @ionic/core package package: angular @ionic/angular package labels Oct 21, 2026

This branch was successfully deployed

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

Labels

package: angular @ionic/angular package package: core @ionic/core package

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant