#4600·rsuite

ButtonGroup does not treat Badge-wrapped buttons as group items

Author: welkinwongCreated Sep 16, 2026Updated Sep 16, 2026

What version of rsuite are you using?

6.2.4

What version of React are you using?

19.3.0

What version of TypeScript are you using (if any)?

6.0.3

What browser are you using?

Chrome

Describe the Bug

Badge is documented as a wrapper around another element (for example a Button). That pattern does not work inside ButtonGroup.

ButtonGroup styles only target direct .rs-btn children:

scss
.rs-btn-group:not([data-vertical='true']) {
  > .rs-btn { /* radius, float */ }
  > .rs-btn[data-appearance='ghost'] + .rs-btn[data-appearance='ghost'] { /* collapse border */ }
}

.rs-btn-group[data-justified='true'] {
  > .rs-btn {
    flex: 1 1 1%;
  }
}

Wrapping a button with Badge changes the group’s direct child from .rs-btn to .rs-badge. The inner button then drops out of:

  1. Justified layout — the item does not get flex: 1 1 1%, so it no longer shares width with sibling buttons.
  2. Connected border-radius:first-child / :last-child apply to the Badge, not the inner button. The wrapped button keeps all four corners rounded; neighbors can also get the wrong corners.
  3. Ghost border collapse — adjacent-sibling rules only match .rs-btn + .rs-btn, so a Badge in the middle leaves a double border.
  4. Focus / active stackingz-index is only set on > .rs-btn.

The same gap exists for vertical and divided groups.

Putting the Badge inside the button still works for an inline count. Wrapping the button is the documented overlay usage, and that is what breaks.

Expected Behavior

A ButtonGroup item that is <Badge><Button /></Badge> should participate in the same group layout as a bare Button:

  • equal width when justified
  • shared / collapsed corners
  • collapsed ghost borders (margin-inline-start: -1px)
  • the inner button stretching to fill the item

To Reproduce

typescript
import { Badge, Button, ButtonGroup } from 'rsuite';
import 'rsuite/dist/rsuite.min.css';

export default function App() {
  return (
    <div style={{ width: 360, padding: 16 }}>
      <ButtonGroup justified>
        <Button appearance="ghost">Doc</Button>
        <Button appearance="ghost">Folder</Button>
        <Badge color="orange" content="3/3">
          <Button appearance="ghost">Knowledge</Button>
        </Badge>
        <Button appearance="ghost">Template</Button>
      </ButtonGroup>
    </div>
  );
}

Compare with the same group without the Badge wrapper: four equal, connected ghost buttons.

Possible fix

Treat a direct-child .rs-badge that wraps a .rs-btn as a group item, mirroring the existing > .rs-btn rules. Something like:

css
.rs-btn-group > .rs-badge {
  position: relative;
  z-index: 3;
}

.rs-btn-group:not([data-vertical='true']) > .rs-badge {
  float: inline-start;
}

.rs-btn-group:not([data-vertical='true']) > .rs-badge:not(:last-child) > .rs-btn {
  border-end-end-radius: 0;
  border-start-end-radius: 0;
}

.rs-btn-group:not([data-vertical='true']) > .rs-badge:not(:first-child) > .rs-btn {
  border-end-start-radius: 0;
  border-start-start-radius: 0;
}

.rs-btn-group:not([data-vertical='true']) > .rs-btn[data-appearance='ghost'] + .rs-badge,
.rs-btn-group:not([data-vertical='true']) > .rs-badge + .rs-badge {
  margin-inline-start: -1px;
}

.rs-btn-group:not([data-vertical='true']) > .rs-badge + .rs-btn[data-appearance='ghost'] {
  margin-inline-start: -1px;
}

.rs-btn-group[data-justified='true'] > .rs-badge {
  display: flex;
  flex: 1 1 1%;
}

.rs-btn-group[data-justified='true'] > .rs-badge > .rs-btn {
  width: 100%;
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

Vertical / divided groups would need the matching > .rs-btn counterparts.