Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
408f69b
feat(button): space badges without a position from the label
thetaPC Oct 23, 2026
cdd1add
chore(): add updated snapshots
Ionitron Oct 23, 2026
025dc92
Merge remote-tracking branch 'origin/FW-6838-ios-sync-snapshots' into…
thetaPC Oct 26, 2026
dd3ef66
fix(button): keep badges above neighboring buttons
thetaPC Oct 26, 2026
15b2ee0
test(button): check that a badge stays above the next button
thetaPC Oct 26, 2026
4823b85
Merge remote-tracking branch 'origin/FW-6838-ios-sync-snapshots' into…
thetaPC Oct 26, 2026
2c4d695
Merge remote-tracking branch 'origin/FW-6838-ios-sync-snapshots' into…
thetaPC Oct 27, 2026
419d5cd
Merge remote-tracking branch 'origin/FW-6838-ios-sync-snapshots' into…
thetaPC Oct 27, 2026
122c48c
Merge remote-tracking branch 'origin/FW-6838-ios-sync-snapshots' into…
thetaPC Oct 27, 2026
59c8d26
Merge remote-tracking branch 'origin/FW-6838-ios-sync-snapshots' into…
thetaPC Oct 27, 2026
8ea6ea2
Merge remote-tracking branch 'origin/FW-6838-ios-sync-snapshots' into…
thetaPC Oct 27, 2026
30f2bfa
Merge remote-tracking branch 'origin/FW-6838-ios-sync-snapshots' into…
thetaPC Oct 28, 2026
e3b9d06
test(button): use ION_BUTTON_SIZES in the badge stacking test
thetaPC Oct 28, 2026
ff71f79
Merge remote-tracking branch 'origin/FW-6838-ios-sync-snapshots' into…
thetaPC Oct 28, 2026
a65efc6
Merge remote-tracking branch 'origin/FW-6838-ios-sync-snapshots' into…
thetaPC Oct 28, 2026
ccaf44a
Merge remote-tracking branch 'origin/FW-6838-ios-sync' into FW-6838-b…
thetaPC Oct 28, 2026
9aafc40
Merge remote-tracking branch 'origin/FW-6838-ios-sync-snapshots' into…
thetaPC Oct 28, 2026
c39e117
Merge remote-tracking branch 'origin/FW-6838-ios-sync-snapshots' into…
thetaPC Oct 28, 2026
06c1d44
test(button): fit the no-position badge screenshot to its content
thetaPC Oct 28, 2026
f8f0a48
test(button): size the no-position badge viewport to the content
thetaPC Oct 28, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 6 additions & 5 deletions BREAKING.md
Original file line number Diff line number Diff line change
Expand Up @@ -59,11 +59,12 @@ The following breaking changes apply to `ion-button`:
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. <sup>[6](#version-10x-button-md-material-design-2)</sup>
7. The `ios` button follows iOS 17 for its font, icons and disabled state. <sup>[7](#version-10x-button-ios-17)</sup>
8. CSS variables have been replaced. <sup>[8](#version-10x-button-replaced-css-variables)</sup>
9. `--padding-*` CSS variables are no longer part of the documented public API (but remain functional). <sup>[9](#version-10x-button-padding-variables)</sup>
10. The fill, size, shape and expand classes have been renamed. <sup>[10](#version-10x-button-renamed-classes)</sup>
11. The `default` size has been deprecated. <sup>[11](#version-10x-button-deprecated-default-size)</sup>
12. Theme classes (`ion-button.md`, `ion-button.ios`) are no longer supported. <sup>[12](#version-10x-button-theme-classes)</sup>
8. A badge without a `vertical` position now has a gap between it and the label, and no longer makes the button taller than its `min.height`. To remove the gap, set `IonButton.size.<size>.badge.margin.start` for global styles or `--ion-button-size-<size>-badge-margin-start` for component-specific styles to `0`.
9. CSS variables have been replaced. <sup>[9](#version-10x-button-replaced-css-variables)</sup>
10. `--padding-*` CSS variables are no longer part of the documented public API (but remain functional). <sup>[10](#version-10x-button-padding-variables)</sup>
11. The fill, size, shape and expand classes have been renamed. <sup>[11](#version-10x-button-renamed-classes)</sup>
12. The `default` size has been deprecated. <sup>[12](#version-10x-button-deprecated-default-size)</sup>
13. Theme classes (`ion-button.md`, `ion-button.ios`) are no longer supported. <sup>[13](#version-10x-button-theme-classes)</sup>

<h5 id="version-10x-button-md-material-design-2">Material Design 2 in <code>md</code></h5>

Expand Down
12 changes: 12 additions & 0 deletions core/api.txt
Original file line number Diff line number Diff line change
Expand Up @@ -580,6 +580,10 @@ ion-button,css-prop,--ion-button-shape-crisp-border-radius
ion-button,css-prop,--ion-button-shape-rectangular-border-radius
ion-button,css-prop,--ion-button-shape-round-border-radius
ion-button,css-prop,--ion-button-shape-soft-border-radius
ion-button,css-prop,--ion-button-size-large-badge-margin-bottom
ion-button,css-prop,--ion-button-size-large-badge-margin-end
ion-button,css-prop,--ion-button-size-large-badge-margin-start
ion-button,css-prop,--ion-button-size-large-badge-margin-top
ion-button,css-prop,--ion-button-size-large-font-size
ion-button,css-prop,--ion-button-size-large-icon-font-size
ion-button,css-prop,--ion-button-size-large-icon-leading-margin-bottom
Expand Down Expand Up @@ -619,6 +623,10 @@ ion-button,css-prop,--ion-button-size-large-spinner-trailing-margin-bottom
ion-button,css-prop,--ion-button-size-large-spinner-trailing-margin-end
ion-button,css-prop,--ion-button-size-large-spinner-trailing-margin-start
ion-button,css-prop,--ion-button-size-large-spinner-trailing-margin-top
ion-button,css-prop,--ion-button-size-medium-badge-margin-bottom
ion-button,css-prop,--ion-button-size-medium-badge-margin-end
ion-button,css-prop,--ion-button-size-medium-badge-margin-start
ion-button,css-prop,--ion-button-size-medium-badge-margin-top
ion-button,css-prop,--ion-button-size-medium-font-size
ion-button,css-prop,--ion-button-size-medium-icon-font-size
ion-button,css-prop,--ion-button-size-medium-icon-leading-margin-bottom
Expand Down Expand Up @@ -658,6 +666,10 @@ ion-button,css-prop,--ion-button-size-medium-spinner-trailing-margin-bottom
ion-button,css-prop,--ion-button-size-medium-spinner-trailing-margin-end
ion-button,css-prop,--ion-button-size-medium-spinner-trailing-margin-start
ion-button,css-prop,--ion-button-size-medium-spinner-trailing-margin-top
ion-button,css-prop,--ion-button-size-small-badge-margin-bottom
ion-button,css-prop,--ion-button-size-small-badge-margin-end
ion-button,css-prop,--ion-button-size-small-badge-margin-start
ion-button,css-prop,--ion-button-size-small-badge-margin-top
ion-button,css-prop,--ion-button-size-small-font-size
ion-button,css-prop,--ion-button-size-small-icon-font-size
ion-button,css-prop,--ion-button-size-small-icon-leading-margin-bottom
Expand Down
12 changes: 12 additions & 0 deletions core/src/components/button/_button.docs.scss
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,10 @@
* @prop --ion-button-size-small-spinner-trailing-margin-end: Right margin if direction is left-to-right, and left margin if direction is right-to-left of a spinner in the `end` slot of the `small` button size
* @prop --ion-button-size-small-spinner-trailing-margin-bottom: Bottom margin of a spinner in the `end` slot of the `small` button size
* @prop --ion-button-size-small-spinner-trailing-margin-start: Left margin if direction is left-to-right, and right margin if direction is right-to-left of a spinner in the `end` slot of the `small` button size
* @prop --ion-button-size-small-badge-margin-top: Top margin of a badge without a `vertical` position in a text button of the `small` size
* @prop --ion-button-size-small-badge-margin-end: Right margin if direction is left-to-right, and left margin if direction is right-to-left of a badge without a `vertical` position in a text button of the `small` size
* @prop --ion-button-size-small-badge-margin-bottom: Bottom margin of a badge without a `vertical` position in a text button of the `small` size
* @prop --ion-button-size-small-badge-margin-start: Left margin if direction is left-to-right, and right margin if direction is right-to-left of a badge without a `vertical` position in a text button of the `small` size
* @prop --ion-button-size-medium-min-width: Minimum width of the `medium` button size
* @prop --ion-button-size-medium-min-height: Minimum height of the `medium` button size, and the width and height of an icon-only button of that size
* @prop --ion-button-size-medium-font-size: Font size of the `medium` button size
Expand Down Expand Up @@ -91,6 +95,10 @@
* @prop --ion-button-size-medium-spinner-trailing-margin-end: Right margin if direction is left-to-right, and left margin if direction is right-to-left of a spinner in the `end` slot of the `medium` button size
* @prop --ion-button-size-medium-spinner-trailing-margin-bottom: Bottom margin of a spinner in the `end` slot of the `medium` button size
* @prop --ion-button-size-medium-spinner-trailing-margin-start: Left margin if direction is left-to-right, and right margin if direction is right-to-left of a spinner in the `end` slot of the `medium` button size
* @prop --ion-button-size-medium-badge-margin-top: Top margin of a badge without a `vertical` position in a text button of the `medium` size
* @prop --ion-button-size-medium-badge-margin-end: Right margin if direction is left-to-right, and left margin if direction is right-to-left of a badge without a `vertical` position in a text button of the `medium` size
* @prop --ion-button-size-medium-badge-margin-bottom: Bottom margin of a badge without a `vertical` position in a text button of the `medium` size
* @prop --ion-button-size-medium-badge-margin-start: Left margin if direction is left-to-right, and right margin if direction is right-to-left of a badge without a `vertical` position in a text button of the `medium` size
* @prop --ion-button-size-large-min-width: Minimum width of the `large` button size
* @prop --ion-button-size-large-min-height: Minimum height of the `large` button size, and the width and height of an icon-only button of that size
* @prop --ion-button-size-large-font-size: Font size of the `large` button size
Expand Down Expand Up @@ -130,6 +138,10 @@
* @prop --ion-button-size-large-spinner-trailing-margin-end: Right margin if direction is left-to-right, and left margin if direction is right-to-left of a spinner in the `end` slot of the `large` button size
* @prop --ion-button-size-large-spinner-trailing-margin-bottom: Bottom margin of a spinner in the `end` slot of the `large` button size
* @prop --ion-button-size-large-spinner-trailing-margin-start: Left margin if direction is left-to-right, and right margin if direction is right-to-left of a spinner in the `end` slot of the `large` button size
* @prop --ion-button-size-large-badge-margin-top: Top margin of a badge without a `vertical` position in a text button of the `large` size
* @prop --ion-button-size-large-badge-margin-end: Right margin if direction is left-to-right, and left margin if direction is right-to-left of a badge without a `vertical` position in a text button of the `large` size
* @prop --ion-button-size-large-badge-margin-bottom: Bottom margin of a badge without a `vertical` position in a text button of the `large` size
* @prop --ion-button-size-large-badge-margin-start: Left margin if direction is left-to-right, and right margin if direction is right-to-left of a badge without a `vertical` position in a text button of the `large` size
* @prop --ion-button-shape-crisp-border-radius: Border radius of the `crisp` button shape
* @prop --ion-button-shape-soft-border-radius: Border radius of the `soft` button shape
* @prop --ion-button-shape-round-border-radius: Border radius of the `round` button shape
Expand Down
5 changes: 5 additions & 0 deletions core/src/components/button/button.interface.ts
Original file line number Diff line number Diff line change
Expand Up @@ -238,6 +238,11 @@ type IonButtonSizeDefinition = {
icon?: IonButtonIconDefinition;
iconOnly?: IonButtonIconOnlyDefinition;
spinner?: IonButtonSpinnerDefinition;

/** A badge without a `vertical` position, which sits after the label of a text button */
badge?: {
margin?: IonMargin;
};
};

type IonButtonShapeDefinition = {
Expand Down
21 changes: 21 additions & 0 deletions core/src/components/button/button.scss
Original file line number Diff line number Diff line change
Expand Up @@ -483,6 +483,27 @@ $spinner-sizes: xsmall, small, medium, large, xlarge;
// Button: With a Badge
// --------------------------------------------------

// A badge can overflow its button, so the button sits above its siblings to keep the badge visible
:host(.button-has-badge) {
position: relative;

z-index: 1;
}

:host(.button-has-badge) .button-native {
// Keeps a badge taller than the label from making the button taller than its min height
@include mixins.padding(var(--padding-top, 0), null, var(--padding-bottom, 0), null);

overflow: visible;
}

@each $size in $sizes {
:host(.button-size-#{$size}:not(.button-has-icon-only)) ::slotted(ion-badge:not([vertical])) {
@include mixins.margin(
var(--ion-button-size-#{$size}-badge-margin-top),
var(--ion-button-size-#{$size}-badge-margin-end),
var(--ion-button-size-#{$size}-badge-margin-bottom),
var(--ion-button-size-#{$size}-badge-margin-start)
);
}
}
88 changes: 88 additions & 0 deletions core/src/components/button/test/badge/button.e2e.ts
Original file line number Diff line number Diff line change
Expand Up @@ -83,5 +83,93 @@ configs({ modes: ['md', 'ios', 'ionic-md'] }).forEach(({ config, screenshot, tit

await expect(container).toHaveScreenshot(screenshot(`button-badge`));
});

test('should space a badge without a position from the label', async ({ page }) => {
const rows = ION_BUTTON_SIZES.map(
(buttonSize) => `<div class="row">${contents
.flatMap((html) =>
ION_BADGE_SIZES.map(
(badgeSize) => `<ion-button size="${buttonSize}" shape="round">
Button
<ion-badge hue="bold" shape="round" color="danger" size="${badgeSize}">${html}</ion-badge>
</ion-button>`
)
)
.join('\n')}
</div>`
).join('\n');

await page.setContent(
`
<style>
.row {
display: grid;
grid-template-columns: repeat(3, auto);
justify-items: start;
align-items: center;
padding-bottom: 10px;
row-gap: 5px;
column-gap: 30px;
}

.badge-container {
width: max-content;
}
</style>

<div class="badge-container">
${rows}
</div>
`,
config
);

const container = page.locator('.badge-container');

// Content wider than the viewport is cut off at the left edge in rtl
const box = await container.boundingBox();
await page.setViewportSize({ width: Math.ceil(box!.width), height: Math.ceil(box!.height) });

await expect(container).toHaveScreenshot(screenshot(`button-badge-no-position`));
});
});
});

/**
* Stacking does not vary across modes or directions
*/
configs({ modes: ['md'], directions: ['ltr'] }).forEach(({ title, config }) => {
test.describe(title('button: badge stacking'), () => {
test('should keep a badge above the next button', async ({ page }) => {
await page.setContent(
`
<div style="display: flex">
<ion-button id="with-badge">
Button
<ion-badge vertical="top">999+</ion-badge>
</ion-button>
<ion-button id="next">Button</ion-button>
</div>
`,
config
);

const badgeBox = (await page.locator('#with-badge ion-badge').boundingBox())!;
const nextBox = (await page.locator('#next').boundingBox())!;

// The area where the badge overflows into the next button
const left = Math.max(badgeBox.x, nextBox.x);
const right = Math.min(badgeBox.x + badgeBox.width, nextBox.x + nextBox.width);
const top = Math.max(badgeBox.y, nextBox.y);
const bottom = Math.min(badgeBox.y + badgeBox.height, nextBox.y + nextBox.height);
expect(right).toBeGreaterThan(left);
expect(bottom).toBeGreaterThan(top);

const topmost = await page.evaluate(
([x, y]) => document.elementFromPoint(x, y)?.closest('ion-badge, ion-button')?.tagName,
[(left + right) / 2, (top + bottom) / 2]
);
expect(topmost).toBe('ION-BADGE');
});
});
});
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
27 changes: 27 additions & 0 deletions core/src/themes/ionic/default.tokens.ts
Original file line number Diff line number Diff line change
Expand Up @@ -666,6 +666,15 @@ export const defaultTheme: DefaultTheme = {
},
},
},

badge: {
margin: {
top: 'var(--ion-spacing-0)',
end: 'var(--ion-spacing-0)',
bottom: 'var(--ion-spacing-0)',
start: 'var(--ion-spacing-sm)',
},
},
},

medium: {
Expand Down Expand Up @@ -771,6 +780,15 @@ export const defaultTheme: DefaultTheme = {
},
},
},

badge: {
margin: {
top: 'var(--ion-spacing-0)',
end: 'var(--ion-spacing-0)',
bottom: 'var(--ion-spacing-0)',
start: 'var(--ion-spacing-250)',
},
},
},

large: {
Expand Down Expand Up @@ -876,6 +894,15 @@ export const defaultTheme: DefaultTheme = {
},
},
},

badge: {
margin: {
top: 'var(--ion-spacing-0)',
end: 'var(--ion-spacing-0)',
bottom: 'var(--ion-spacing-0)',
start: 'var(--ion-spacing-md)',
},
},
},
},

Expand Down
27 changes: 27 additions & 0 deletions core/src/themes/ios/default.tokens.ts
Original file line number Diff line number Diff line change
Expand Up @@ -635,6 +635,15 @@ export const defaultTheme: DefaultTheme = {
},
},
},

badge: {
margin: {
top: '0',
end: '0',
bottom: '0',
start: 'var(--ion-spacing-xxs)',
},
},
},

medium: {
Expand Down Expand Up @@ -725,6 +734,15 @@ export const defaultTheme: DefaultTheme = {
},
},
},

badge: {
margin: {
top: '0',
end: '0',
bottom: '0',
start: 'var(--ion-spacing-xxs)',
},
},
},

large: {
Expand Down Expand Up @@ -815,6 +833,15 @@ export const defaultTheme: DefaultTheme = {
},
},
},

badge: {
margin: {
top: '0',
end: '0',
bottom: '0',
start: 'var(--ion-spacing-xxs)',
},
},
},
},

Expand Down
27 changes: 27 additions & 0 deletions core/src/themes/md/default.tokens.ts
Original file line number Diff line number Diff line change
Expand Up @@ -694,6 +694,15 @@ export const defaultTheme: DefaultTheme = {
},
},
},

badge: {
margin: {
top: '0',
end: '0',
bottom: '0',
start: 'var(--ion-spacing-sm)',
},
},
},

medium: {
Expand Down Expand Up @@ -786,6 +795,15 @@ export const defaultTheme: DefaultTheme = {
},
},
},

badge: {
margin: {
top: '0',
end: '0',
bottom: '0',
start: 'var(--ion-spacing-sm)',
},
},
},

large: {
Expand Down Expand Up @@ -874,6 +892,15 @@ export const defaultTheme: DefaultTheme = {
},
},
},

badge: {
margin: {
top: '0',
end: '0',
bottom: '0',
start: 'var(--ion-spacing-md)',
},
},
},
},

Expand Down
Loading