Skip to content

feat(button): improve badge spacing and stacking - #31647

Open
thetaPC wants to merge 32 commits into
FW-6838-ios-sync-snapshotsfrom
FW-6838-badge-spacing
Open

thetaPC wants to merge 32 commits into
FW-6838-ios-sync-snapshotsfrom
FW-6838-badge-spacing

Conversation

@thetaPC

@thetaPC thetaPC commented Oct 23, 2026 •

Copy link
Copy Markdown
Contributor

Issue number: resolves internal


What is the current behavior?

A badge without a vertical position sits against the button's label with no gap. In md and ios, a badge taller than the label also makes the button taller, so buttons of the same size have different heights. A badge with a vertical position that overflows its button can also be covered by the button next to it.

What is the new behavior?

  • A badge without a vertical position in a text button has a gap from the label, set by the new IonButton.size.<size>.badge.margin token: 8px, 10px and 12px in ionic, 8px, 8px and 12px in md, and 4px in ios, the same as each theme's icon gap
  • A badge without a vertical position overlaps the button's vertical padding, so a tall badge no longer makes the button taller than its min.height. The button keeps its padding, so wrapped labels and vertical badges keep it too
  • A badge in the start slot keeps its gap on the label's side
  • A button with a badge sits above the buttons beside it, so a badge that overflows its button stays visible instead of being covered by the next button
  • Icon-only buttons and the placement of badges with a vertical position don't change

Does this introduce a breaking change?

  • Yes
  • No

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.

Other information

Known gaps

  • Two buttons that both have badges can still cover each other's badges. Each button is its own stacking context, so a badge can't rise above another button that also sits on top. A full fix means removing the two rules that create that context: contain: layout on the native element, added for the unbounded ripple in feat(ripple): adds unbounded ripple-effect #16399, and z-index: 1 on the inner element, added for a state overlay in feat(components): improve hover states for buttons #19440 that the migration removed. That changes how every button paints, including the ripple's layering and possibly its performance, and badges would then paint over other page content like sticky list dividers. It needs its own investigation and a full screenshot sweep, so it's tracked in FW-7920

How to test

Check Preview What to look for
Badges without a position ios, md, ionic In the "badge position: none" sections, text buttons have a gap before the badge, and every button in a row is the same height

The stacking case has no test page: every button on the badge page has a badge. The badge spec checks it instead ("should keep a badge above the next button"), and it fails without the fix.

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

vercel Bot commented Oct 23, 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 10, 2026 1:43am UTC

Request Review

@github-actions github-actions Bot added the package: core @ionic/core package label Oct 23, 2026
@thetaPC thetaPC changed the title feat(button): space badges without a position from the label feat(button): improve badge spacing and stacking Oct 26, 2026
Base automatically changed from FW-6838-ios-sync-snapshots to ionic-modular October 26, 2026 22:54
@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, reusing button-has-badge keeps this simple and the new snapshots look right! Just a couple of things, mainly badge buttons with a wrapped label losing their padding.

z-index: 1;
}

:host(.button-has-badge) .button-native {

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.

Dropping the vertical padding for the whole button hits badge buttons whose label wraps or scales up. With a two-line label in a 140px expand="block" button, the text ends up 3px from the top and bottom edges, where the same button without a badge keeps 7px. Could the compensation go on the badge instead, like a negative block margin on ::slotted(ion-badge:not([vertical]))? The label would keep its padding, and buttons with a vertical badge wouldn't lose it either.

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 call, moved it to the badge in c8db919. The button keeps its padding, and a badge without a vertical position gets negative block margins equal to that padding, so a tall badge still doesn't make the button taller than its min height. Your wrapped expand="block" example now keeps 7px, and vertical badges keep the padding too.

The badge.margin top and bottom tokens add to those negative margins, so custom themes can still adjust them. No theme sets them, and they fall back to 0px, since an unset or unitless value would make the calc() invalid. The other zero margins in the button tokens now use var(--ion-spacing-0) (ded6629).

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.

Thanks, the wrapped button keeps its 7px now! Could we add a screenshot for it though? None of the badge screenshots have a wrapped label, so going back to zeroing the .button-native padding still passes all of them, even though a two-line expand="block" button with a badge in a 140px container comes out 8px shorter in md and ios (60px to 52px in md). One of those in the button-badge-no-position test would catch it.

}

@each $size in $sizes {
:host(.button-size-#{$size}:not(.button-has-icon-only)) ::slotted(ion-badge:not([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.

A badge in slot="start" matches this too, so the gap ends up between the button edge and the badge (16px to 24px) while the badge still sits right against the label. Should this skip [slot="start"], or give start-slot badges an end margin instead?

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.

Gave start-slot badges swapped inline margins in 6a8940f, so the gap is now between the badge and the label. The @prop docs and the recipe comment now describe start as the gap on the label's side and end as the other side.

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.

Thanks, the gap is on the label's side now! There's no screenshot with a slot="start" badge though, so removing these rules still passes every badge screenshot even though the gap moves back to the outer side. Could you add a row of start-slot badge buttons to the button-badge-no-position test too?

@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 good to me, thanks for the fixes, just one small follow-up on the screenshots!

z-index: 1;
}

:host(.button-has-badge) .button-native {

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.

Thanks, the wrapped button keeps its 7px now! Could we add a screenshot for it though? None of the badge screenshots have a wrapped label, so going back to zeroing the .button-native padding still passes all of them, even though a two-line expand="block" button with a badge in a 140px container comes out 8px shorter in md and ios (60px to 52px in md). One of those in the button-badge-no-position test would catch it.

}

@each $size in $sizes {
:host(.button-size-#{$size}:not(.button-has-icon-only)) ::slotted(ion-badge:not([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.

Thanks, the gap is on the label's side now! There's no screenshot with a slot="start" badge though, so removing these rules still passes every badge screenshot even though the gap moves back to the outer side. Could you add a row of start-slot badge buttons to the button-badge-no-position test too?

… FW-6838-badge-spacing

# Conflicts:
#	BREAKING.md

This branch was successfully deployed

1 active deployment
Preview — 25e1a78b Deployed Nov 10, 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.

3 participants