From ac4ba25b7484dd5b859efcc020b183aa2a5b0286 Mon Sep 17 00:00:00 2001
From: Maria Hutt
Date: Fri, 23 Oct 2026 15:09:09 -0700
Subject: [PATCH 1/3] feat(button): sync the ios theme with iOS 17
---
BREAKING.md | 31 +++-
core/src/themes/ios/default.tokens.ts | 231 +++++++++++++++++++++-----
2 files changed, 217 insertions(+), 45 deletions(-)
diff --git a/BREAKING.md b/BREAKING.md
index 72c924caa1d..6f0a5ad6bc8 100644
--- a/BREAKING.md
+++ b/BREAKING.md
@@ -55,14 +55,15 @@ The following breaking changes apply to `ion-button`:
1. The `border-radius` of the `ios` button is now `6px` at every size, instead of `6px`, `14px` and `16px` for the small, default and large sizes. The `md` button now defaults to `999px` instead of `4px`. Both follow the iOS and Material Design 3 guidelines. To revert to the previous appearance, set the `shape` to `"soft"` for `md`. For `ios`, set `IonButton.shape.soft.border.radius` for global styles or `--ion-button-shape-soft-border-radius` for component-specific styles to `14px`, and to `6px` and `16px` on small and large buttons.
2. Icon-only buttons in the `start` and `end` slots of `ion-input`, `ion-select` and `ion-textarea` now follow the button's `shape` instead of always being circles. For `ios`, set the `shape` to `"round"` to keep them circular.
3. The font size of `clear` buttons in the `ios` theme now matches the other fills at each size, instead of `17px` at the default size. To keep the previous size, set `--ion-button-size-medium-font-size` to `17px` on those buttons.
-4. The `ios` button is now `28px`, `34px` and `50px` tall at the small, default and large sizes, with less padding, following the iOS guidelines. To revert to the previous appearance, set `IonButton.size..min.height` for global styles or `--ion-button-size--min-height` for component-specific styles to `2.1em`, `3.1em` and `3.1em`, and the padding (`IonButton.size..padding` or `--ion-button-size--padding-*`) to `4px 0.9em`, `13px 1em` and `17px 1em`.
+4. The `ios` button is now `28px`, `34px` and `50px` tall at the small, default and large sizes, with less padding, following the iOS guidelines. To revert to the previous appearance, set `IonButton.size..min.height` for global styles or `--ion-button-size--min-height` for component-specific styles to `2.1em`, `3.1em` and `3.1em`, and the padding (`IonButton.size..padding` or `--ion-button-size--padding-*`) to `4px 0.9em`, `13px 1em` and `17px 1em`, along with the previous font sizes from item 7.
5. Icon-only buttons are now squares as tall as a text button of the same size. In `md`, this makes them a few pixels smaller at the small and default sizes and larger at the large size. To change their size, set the size's `min.height` as described above.
6. The `md` button follows Material Design 2 for its spacing, outline, elevation and disabled state. [6](#version-10x-button-md-material-design-2)
-7. CSS variables have been replaced. [7](#version-10x-button-replaced-css-variables)
-8. `--padding-*` CSS variables are no longer part of the documented public API (but remain functional). [8](#version-10x-button-padding-variables)
-9. The fill, size, shape and expand classes have been renamed. [9](#version-10x-button-renamed-classes)
-10. The `default` size has been deprecated. [10](#version-10x-button-deprecated-default-size)
-11. Theme classes (`ion-button.md`, `ion-button.ios`) are no longer supported. [11](#version-10x-button-theme-classes)
+7. The `ios` button follows iOS 17 for its font, icons and disabled state. [7](#version-10x-button-ios-17)
+8. CSS variables have been replaced. [8](#version-10x-button-replaced-css-variables)
+9. `--padding-*` CSS variables are no longer part of the documented public API (but remain functional). [9](#version-10x-button-padding-variables)
+10. The fill, size, shape and expand classes have been renamed. [10](#version-10x-button-renamed-classes)
+11. The `default` size has been deprecated. [11](#version-10x-button-deprecated-default-size)
+12. Theme classes (`ion-button.md`, `ion-button.ios`) are no longer supported. [12](#version-10x-button-theme-classes)
@@ -82,6 +83,24 @@ The `md` button now uses the Material Design 2 values below. To keep a previous
Buttons in toolbars keep their previous colors when disabled and have no elevation on hover or focus.
+
+
+The `ios` button now uses the iOS 17 values below. To keep a previous value, set the token for global styles, or the matching `--ion-button-*` CSS variable for component-specific styles. Replace `` with `solid`, `outline` or `clear`, and `` with `small`, `medium` or `large`. Values in `em` are relative to the font size, so reverting the font size also needs the previous `em` values.
+
+| Property | Previous | New | Token |
+|---|---|---|---|
+| Font size | `13px`, `16px` and `20px` | `15px`, `15px` and `17px` | `IonButton.size..font.size` |
+| Font weight of solid and outline | `500` | `400` | `IonButton.hue.bold..default.font.weight` |
+| Horizontal padding | `0.77em`, `0.875em` and `1em` | `0.6667em`, `0.9333em` and `1.1765em` (same pixels at the new font size) | `IonButton.size..padding.start` and `.end` |
+| Min height | `2.16em`, `2.125em` and `2.5em` | `1.8667em`, `2.2667em` and `2.9412em` (same pixels at the new font size) | `IonButton.size..min.height` |
+| Icon size next to a label | `1.35em` | `1.2em` | `IonButton.size..icon.font.size` |
+| Icon spacing | `0.3em` into the padding and a `0.3em` gap | at the padding edge and a `0.2222em` gap (`4px` at the default size) | `IonButton.size..icon.leading.margin` and `.trailing.margin` |
+| Icon-only icon size | `1.308125em`, `1.125em` and `0.9em` | `1.1333em`, `1.2em` and `1.0588em` (same pixels at the new font size) | `IonButton.size..iconOnly.icon.font.size` |
+| Spinner in a button | the spinner's own size (`28px` by default) | `1.2em` by default (`18px` at the default size), with the other spinner sizes in their usual ratio to it | `IonButton.size..spinner.size..width` and `.height` |
+| Disabled | the whole button at `0.5` opacity | the text color at `25%`, which outline also uses for its border, and on solid, an `8%` container | `IonButton.hue.bold..disabled.opacity`, `.color`, `.background` and `.border.color` |
+
+Buttons in toolbars keep their previous colors when disabled.
+
Use the new token structure for global styles, or the corresponding CSS variable for component-specific overrides. Replace `` with `solid`, `outline` or `clear`, and `` with `crisp`, `soft`, `round` or `rectangular`. Buttons with a `color` read the same tokens under `semantic`, for example `IonButton.hue.bold.solid.semantic.default.background`.
diff --git a/core/src/themes/ios/default.tokens.ts b/core/src/themes/ios/default.tokens.ts
index 4e5c43c0d18..53668718af2 100644
--- a/core/src/themes/ios/default.tokens.ts
+++ b/core/src/themes/ios/default.tokens.ts
@@ -336,7 +336,7 @@ export const defaultTheme: DefaultTheme = {
color: ionColor('primary', 'contrast'),
font: {
- weight: 'var(--ion-font-weight-medium)',
+ weight: 'var(--ion-font-weight-normal)',
},
},
@@ -356,9 +356,8 @@ export const defaultTheme: DefaultTheme = {
},
disabled: {
- background: ionColor('primary', 'base'),
-
- opacity: '0.5',
+ background: rgba('var(--ion-text-color-rgb, 0, 0, 0)', 0.08),
+ color: rgba('var(--ion-text-color-rgb, 0, 0, 0)', 0.25),
},
// Any of the semantic colors like primary, secondary, etc.
@@ -384,7 +383,8 @@ export const defaultTheme: DefaultTheme = {
},
disabled: {
- background: currentColor('base'),
+ background: rgba('var(--ion-text-color-rgb, 0, 0, 0)', 0.08),
+ color: rgba('var(--ion-text-color-rgb, 0, 0, 0)', 0.25),
},
},
},
@@ -407,7 +407,7 @@ export const defaultTheme: DefaultTheme = {
},
font: {
- weight: 'var(--ion-font-weight-medium)',
+ weight: 'var(--ion-font-weight-normal)',
},
},
@@ -431,7 +431,11 @@ export const defaultTheme: DefaultTheme = {
disabled: {
background: 'transparent',
- opacity: '0.5',
+ border: {
+ color: rgba('var(--ion-text-color-rgb, 0, 0, 0)', 0.25),
+ },
+
+ color: rgba('var(--ion-text-color-rgb, 0, 0, 0)', 0.25),
},
semantic: {
@@ -461,6 +465,12 @@ export const defaultTheme: DefaultTheme = {
disabled: {
background: 'transparent',
+
+ border: {
+ color: rgba('var(--ion-text-color-rgb, 0, 0, 0)', 0.25),
+ },
+
+ color: rgba('var(--ion-text-color-rgb, 0, 0, 0)', 0.25),
},
},
},
@@ -502,8 +512,7 @@ export const defaultTheme: DefaultTheme = {
disabled: {
background: 'transparent',
-
- opacity: '0.5',
+ color: rgba('var(--ion-text-color-rgb, 0, 0, 0)', 0.25),
},
semantic: {
@@ -529,6 +538,7 @@ export const defaultTheme: DefaultTheme = {
disabled: {
background: 'transparent',
+ color: rgba('var(--ion-text-color-rgb, 0, 0, 0)', 0.25),
},
},
},
@@ -539,40 +549,40 @@ export const defaultTheme: DefaultTheme = {
size: {
small: {
min: {
- height: 'clamp(23px, 2.16em, 54px)',
+ height: 'clamp(23px, 1.8667em, 54px)',
},
padding: {
top: 'var(--ion-spacing-xxs)',
- end: '0.77em',
+ end: '0.6667em',
bottom: 'var(--ion-spacing-xxs)',
- start: '0.77em',
+ start: '0.6667em',
},
font: {
- size: `min(${dynamicFont(global.root, 13)}, 39px)`,
+ size: `min(${dynamicFont(global.root, 15)}, 45px)`,
},
icon: {
font: {
- size: '1.35em',
+ size: '1.2em',
},
leading: {
margin: {
top: '0',
- end: '0.3em',
+ end: '0.2222em',
bottom: '0',
- start: '-0.3em',
+ start: '0',
},
},
trailing: {
margin: {
top: '0',
- end: '-0.2em',
+ end: '0',
bottom: '0',
- start: '0.3em',
+ start: '0.2222em',
},
},
},
@@ -591,7 +601,37 @@ export const defaultTheme: DefaultTheme = {
* The values were provided through a native iOS app.
* min font size: 12px, default font size: 17px, max font size: 40px
*/
- size: 'clamp(12.1394px, 1.308125em, 40.1856px)',
+ size: 'clamp(12.1394px, 1.1333em, 40.1856px)',
+ },
+ },
+ },
+
+ spinner: {
+ // `medium`, the default, matches the icon; the other sizes keep their standalone ratio to it
+ size: {
+ xsmall: {
+ height: '0.6857em',
+ width: '0.6857em',
+ },
+
+ small: {
+ height: '0.8571em',
+ width: '0.8571em',
+ },
+
+ medium: {
+ height: '1.2em',
+ width: '1.2em',
+ },
+
+ large: {
+ height: '1.5429em',
+ width: '1.5429em',
+ },
+
+ xlarge: {
+ height: '2.0571em',
+ width: '2.0571em',
},
},
},
@@ -599,40 +639,40 @@ export const defaultTheme: DefaultTheme = {
medium: {
min: {
- height: 'clamp(30px, 2.125em, 60px)',
+ height: 'clamp(30px, 2.2667em, 60px)',
},
padding: {
top: '7px',
- end: '0.875em',
+ end: '0.9333em',
bottom: '7px',
- start: '0.875em',
+ start: '0.9333em',
},
font: {
- size: `min(${dynamicFont(global.root, 16)}, 48px)`,
+ size: `min(${dynamicFont(global.root, 15)}, 45px)`,
},
icon: {
font: {
- size: '1.35em',
+ size: '1.2em',
},
leading: {
margin: {
top: '0',
- end: '0.3em',
+ end: '0.2222em',
bottom: '0',
- start: '-0.3em',
+ start: '0',
},
},
trailing: {
margin: {
top: '0',
- end: '-0.2em',
+ end: '0',
bottom: '0',
- start: '0.3em',
+ start: '0.2222em',
},
},
},
@@ -651,7 +691,37 @@ export const defaultTheme: DefaultTheme = {
* The values were provided through a native iOS app.
* min font size: 15px, default font size: 18px, max font size: 43px
*/
- size: 'clamp(15.12px, 1.125em, 43.02px)',
+ size: 'clamp(15.12px, 1.2em, 43.02px)',
+ },
+ },
+ },
+
+ spinner: {
+ // `medium`, the default, matches the icon; the other sizes keep their standalone ratio to it
+ size: {
+ xsmall: {
+ height: '0.6857em',
+ width: '0.6857em',
+ },
+
+ small: {
+ height: '0.8571em',
+ width: '0.8571em',
+ },
+
+ medium: {
+ height: '1.2em',
+ width: '1.2em',
+ },
+
+ large: {
+ height: '1.5429em',
+ width: '1.5429em',
+ },
+
+ xlarge: {
+ height: '2.0571em',
+ width: '2.0571em',
},
},
},
@@ -659,40 +729,40 @@ export const defaultTheme: DefaultTheme = {
large: {
min: {
- height: 'clamp(46px, 2.5em, 78px)',
+ height: 'clamp(46px, 2.9412em, 78px)',
},
padding: {
top: '14px',
- end: '1em',
+ end: '1.1765em',
bottom: '14px',
- start: '1em',
+ start: '1.1765em',
},
font: {
- size: `min(${dynamicFont(global.root, 20)}, 60px)`,
+ size: `min(${dynamicFont(global.root, 17)}, 51px)`,
},
icon: {
font: {
- size: '1.35em',
+ size: '1.2em',
},
leading: {
margin: {
top: '0',
- end: '0.3em',
+ end: '0.2222em',
bottom: '0',
- start: '-0.3em',
+ start: '0',
},
},
trailing: {
margin: {
top: '0',
- end: '-0.2em',
+ end: '0',
bottom: '0',
- start: '0.3em',
+ start: '0.2222em',
},
},
},
@@ -711,7 +781,37 @@ export const defaultTheme: DefaultTheme = {
* The values were provided through a native iOS app.
* min font size: 15px, default font size: 18px, max font size: 43px
*/
- size: 'clamp(15.12px, 0.9em, 43.056px)',
+ size: 'clamp(15.12px, 1.0588em, 43.056px)',
+ },
+ },
+ },
+
+ spinner: {
+ // `medium`, the default, matches the icon; the other sizes keep their standalone ratio to it
+ size: {
+ xsmall: {
+ height: '0.6857em',
+ width: '0.6857em',
+ },
+
+ small: {
+ height: '0.8571em',
+ width: '0.8571em',
+ },
+
+ medium: {
+ height: '1.2em',
+ width: '1.2em',
+ },
+
+ large: {
+ height: '1.5429em',
+ width: '1.5429em',
+ },
+
+ xlarge: {
+ height: '2.0571em',
+ width: '2.0571em',
},
},
},
@@ -835,6 +935,21 @@ export const defaultTheme: DefaultTheme = {
activated: {
color: `var(--ion-toolbar-background, ${ionColor('primary', 'contrast')})`,
},
+
+ // In a toolbar, a disabled button keeps its colors and fades
+ disabled: {
+ background: 'var(--ion-button-hue-bold-solid-default-background)',
+ color: 'var(--ion-button-hue-bold-solid-default-color)',
+
+ opacity: '0.5',
+ },
+
+ semantic: {
+ disabled: {
+ background: 'var(--ion-button-hue-bold-solid-semantic-default-background)',
+ color: 'var(--ion-button-hue-bold-solid-semantic-default-color)',
+ },
+ },
},
outline: {
@@ -858,6 +973,31 @@ export const defaultTheme: DefaultTheme = {
activated: {
background: `var(--ion-toolbar-color, ${ionColor('primary', 'base')})`,
},
+
+ // In a toolbar, a disabled button keeps its colors and fades
+ disabled: {
+ background: 'var(--ion-button-hue-bold-outline-default-background)',
+
+ border: {
+ color: 'var(--ion-button-hue-bold-outline-default-border-color)',
+ },
+
+ color: 'var(--ion-button-hue-bold-outline-default-color)',
+
+ opacity: '0.5',
+ },
+
+ semantic: {
+ disabled: {
+ background: 'var(--ion-button-hue-bold-outline-semantic-default-background)',
+
+ border: {
+ color: 'var(--ion-button-hue-bold-outline-semantic-default-border-color)',
+ },
+
+ color: 'var(--ion-button-hue-bold-outline-semantic-default-color)',
+ },
+ },
},
clear: {
@@ -876,6 +1016,19 @@ export const defaultTheme: DefaultTheme = {
activated: {
color: `var(--ion-toolbar-color, ${ionColor('primary', 'foreground')})`,
},
+
+ // In a toolbar, a disabled button keeps its colors and fades
+ disabled: {
+ color: 'var(--ion-button-hue-bold-clear-default-color)',
+
+ opacity: '0.5',
+ },
+
+ semantic: {
+ disabled: {
+ color: 'var(--ion-button-hue-bold-clear-semantic-default-color)',
+ },
+ },
},
},
},
From 5718f9189fd489004e98d160bc45b5e72e25e4ef Mon Sep 17 00:00:00 2001
From: Maria Hutt
Date: Fri, 23 Oct 2026 15:10:57 -0700
Subject: [PATCH 2/3] fix(button): fill pressed outline buttons in ios toolbars
---
core/src/themes/ios/default.tokens.ts | 4 ----
1 file changed, 4 deletions(-)
diff --git a/core/src/themes/ios/default.tokens.ts b/core/src/themes/ios/default.tokens.ts
index 53668718af2..2a4ae7318f2 100644
--- a/core/src/themes/ios/default.tokens.ts
+++ b/core/src/themes/ios/default.tokens.ts
@@ -1098,10 +1098,6 @@ export const defaultTheme: DefaultTheme = {
hover: {
background: 'transparent',
},
-
- activated: {
- background: 'transparent',
- },
},
clear: {
From 4ad8e0918e3c9feb9bba486e89a15e8702821172 Mon Sep 17 00:00:00 2001
From: Maria Hutt
Date: Wed, 28 Oct 2026 16:00:20 -0700
Subject: [PATCH 3/3] test(button): remove duplicate blank line from the basic
test page
---
core/src/components/button/test/basic/index.html | 1 -
1 file changed, 1 deletion(-)
diff --git a/core/src/components/button/test/basic/index.html b/core/src/components/button/test/basic/index.html
index 8825e8f8e06..726aaf2c138 100644
--- a/core/src/components/button/test/basic/index.html
+++ b/core/src/components/button/test/basic/index.html
@@ -97,7 +97,6 @@
Opacity: 0.2
-
Toggle Disabled