Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
d2a0364
feat(button): show only the icon and vertical badges in icon-only but…
thetaPC Oct 23, 2026
db6eca1
Merge remote-tracking branch 'origin/FW-6838-badge-spacing' into FW-6…
thetaPC Oct 26, 2026
3c8aba6
Merge remote-tracking branch 'origin/FW-6838-badge-spacing' into FW-6…
thetaPC Oct 26, 2026
ba5108a
Merge remote-tracking branch 'origin/FW-6838-badge-spacing' into FW-6…
thetaPC Oct 26, 2026
6c03447
Merge remote-tracking branch 'origin/FW-6838-badge-spacing' into FW-6…
thetaPC Oct 26, 2026
554b0cb
fix(button): hide icon-only text through the default slot instead of …
thetaPC Oct 26, 2026
638bfbc
Merge remote-tracking branch 'origin/FW-6838-badge-spacing' into FW-6…
thetaPC Oct 26, 2026
736ff49
Merge remote-tracking branch 'origin/FW-6838-badge-spacing' into FW-6…
thetaPC Oct 27, 2026
22d07b4
Merge remote-tracking branch 'origin/FW-6838-badge-spacing' into FW-6…
thetaPC Oct 27, 2026
8878102
Merge remote-tracking branch 'origin/FW-6838-badge-spacing' into FW-6…
thetaPC Oct 27, 2026
ba8c040
Merge remote-tracking branch 'origin/FW-6838-badge-spacing' into FW-6…
thetaPC Oct 27, 2026
41f7635
Merge remote-tracking branch 'origin/FW-6838-badge-spacing' into FW-6…
thetaPC Oct 27, 2026
de1c6ae
Merge remote-tracking branch 'origin/FW-6838-badge-spacing' into FW-6…
thetaPC Oct 28, 2026
339daa0
Merge remote-tracking branch 'origin/FW-6838-badge-spacing' into FW-6…
thetaPC Oct 28, 2026
0700826
Merge remote-tracking branch 'origin/FW-6838-badge-spacing' into FW-6…
thetaPC Oct 28, 2026
483ae2f
Merge remote-tracking branch 'origin/FW-6838-badge-spacing' into FW-6…
thetaPC Oct 28, 2026
a8cf631
Merge remote-tracking branch 'origin/FW-6838-badge-spacing' into FW-6…
thetaPC Oct 28, 2026
e371569
Merge remote-tracking branch 'origin/FW-6838-badge-spacing' into FW-6…
thetaPC Oct 28, 2026
791e695
Merge remote-tracking branch 'origin/FW-6838-badge-spacing' into FW-6…
thetaPC Oct 28, 2026
9f6aee1
Merge remote-tracking branch 'origin/FW-6838-badge-spacing' into FW-6…
thetaPC Oct 28, 2026
5feb0ce
Merge remote-tracking branch 'origin/FW-6838-badge-spacing' into FW-6…
thetaPC Oct 28, 2026
ec7aeb9
Merge remote-tracking branch 'origin/FW-6838-badge-spacing' into FW-6…
thetaPC Oct 29, 2026
dd49a78
Merge remote-tracking branch 'origin/FW-6838-badge-spacing' into FW-6…
thetaPC Oct 29, 2026
73bcbf1
Merge remote-tracking branch 'origin/FW-6838-badge-spacing' into FW-6…
thetaPC Oct 29, 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 @@ -63,11 +63,12 @@ The following breaking changes apply to `ion-button`:
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. 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>
9. A button with an icon in the `icon-only` slot now shows only that icon and any badge with a `vertical` position. Text, elements in the `start` and `end` slots, and other content are hidden. Hidden text still names the button for assistive technology. To show other content, remove the `icon-only` slot and use the `start` or `end` slot for the icon.
10. CSS variables have been replaced. <sup>[10](#version-10x-button-replaced-css-variables)</sup>
11. `--padding-*` CSS variables are no longer part of the documented public API (but remain functional). <sup>[11](#version-10x-button-padding-variables)</sup>
12. The fill, size, shape and expand classes have been renamed. <sup>[12](#version-10x-button-renamed-classes)</sup>
13. The `default` size has been deprecated. <sup>[13](#version-10x-button-deprecated-default-size)</sup>
14. Theme classes (`ion-button.md`, `ion-button.ios`) are no longer supported. <sup>[14](#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
17 changes: 17 additions & 0 deletions core/src/components/button/button.scss
Original file line number Diff line number Diff line change
Expand Up @@ -179,6 +179,23 @@ $spinner-sizes: xsmall, small, medium, large, xlarge;
}
}

// An icon-only button shows only its icon and badges with a `vertical` position
:host(.button-has-icon-only) ::slotted([slot="start"]),

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.

Outside CSS that sets display beats these ::slotted() rules, so the start and end content can come back. In the ionic theme the global link style does it, so <a slot="start" href="#x">Link</a> in an icon-only button still renders, gets its own tab stop, and becomes the button's name. Could we skip rendering the start and end slots when hasIconOnly, or make these !important? An <a slot="start"> case in the tests would cover it.

:host(.button-has-icon-only) ::slotted([slot="end"]),
:host(.button-has-icon-only) ::slotted(:not([slot]):not(ion-badge[vertical])) {

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 one worries me a bit, because text wrapped in an element loses its accessible name now. With <span>Add item</span> or <ion-label>Add item</ion-label> next to the icon, the button has an empty name, where before it was "Add item", and only bare text still works. A visually hidden span is a pretty common way to label an icon button, so could the default-slot elements be visually hidden instead of display: none? If not, BREAKING.md should say only bare text names the button, since right now it says hidden text still does. A test with wrapped text would catch this either way.

display: none;
}

/**
* Text can't be selected, so the default slot passes it a zero font size.
* Letter spacing is inherited in px, so it's reset too.
*/
:host(.button-has-icon-only) slot:not([name]) {
font-size: 0;

letter-spacing: 0;
}

// Button: Slotted Elements
// --------------------------------------------------

Expand Down
2 changes: 1 addition & 1 deletion core/src/components/button/button.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ import type { IonButtonExpand, IonButtonFill, IonButtonShape, IonButtonSize } fr
* @virtualProp {"ios" | "md"} mode - The mode determines the platform behaviors of the component.
*
* @slot - Content is placed between the named slots if provided without a slot.
* @slot icon-only - Should be used on an icon in a button that has no text.
* @slot icon-only - Should be used on an icon in a button that has no text. The button then shows only this icon and any badge with a `vertical` position.
* @slot start - Content is placed to the left of the button text in LTR, and to the right in RTL.
* @slot end - Content is placed to the right of the button text in LTR, and to the left in RTL.
*
Expand Down
48 changes: 48 additions & 0 deletions core/src/components/button/test/icon/button.e2e.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,3 +12,51 @@ configs({ modes: ['ios', 'md', 'ionic-md'] }).forEach(({ title, screenshot, conf
});
});
});

/**
* This behavior does not vary across modes or directions
*/
configs({ modes: ['md'], directions: ['ltr'] }).forEach(({ title, config }) => {
test.describe(title('button: icon only'), () => {
test('should only show the icon and badges with a vertical position', async ({ page }) => {
await page.setContent(
`
<ion-button id="plain"><ion-icon slot="icon-only" name="add"></ion-icon></ion-button>
<ion-button id="extra">
<ion-icon slot="start" name="star"></ion-icon>
<ion-icon slot="icon-only" name="add"></ion-icon>
Add item
<ion-badge id="inline-badge">3</ion-badge>
<ion-badge id="vertical-badge" vertical="top">3</ion-badge>
<ion-icon slot="end" name="star"></ion-icon>
</ion-button>
`,
config
);

const extra = page.locator('#extra');

await expect(extra.locator('[slot="start"]')).toBeHidden();
await expect(extra.locator('[slot="end"]')).toBeHidden();
await expect(page.locator('#inline-badge')).toBeHidden();
await expect(page.locator('#vertical-badge')).toBeVisible();

// The hidden text takes no space, so the button stays the size of a plain icon-only button
const plainBox = await page.locator('#plain').boundingBox();
const extraBox = await extra.boundingBox();
expect(extraBox!.width).toBe(plainBox!.width);
expect(extraBox!.height).toBe(plainBox!.height);
});

test('should use hidden text as the accessible name', async ({ page }) => {
await page.setContent(
`
<ion-button><ion-icon slot="icon-only" name="add"></ion-icon>Add item</ion-button>
`,
config
);

await expect(page.getByRole('button')).toHaveAccessibleName('Add item');
});
});
});
Loading