[BUG] - ButtonGroup variant/size props don't propagate to Button when child is wrapped in Tooltip
HeroUI Version
@heroui/[email protected]
Describe the bug
ButtonGroup propagates variant, size, isDisabled, and fullWidth to its child Buttons via two mechanisms in tandem:
- A
ButtonGroupContextprovider. - A
__button_group_child: truemarker prop stamped onto direct children viaChildren.map+cloneElement(packages/components/button-group/src/button-group.tsx).
Button then gates context consumption behind that marker:
// packages/components/button/src/button.tsx
const shouldUseContext = isButtonGroupChild === true;
const finalVariant = variant ?? (shouldUseContext ? buttonGroupContext?.variant : undefined);When a Button is wrapped in any single-element passthrough — most commonly <Tooltip>, but also things like <Popover.Trigger> — the wrapper becomes the direct child of ButtonGroup and absorbs the marker (Tooltip silently drops unknown props). The inner Button never receives the marker, shouldUseContext stays false, and variant / size / isDisabled / fullWidth fall back to component defaults instead of inheriting from the group.
The sibling pair behaves differently: ToggleButtonGroup exports TOGGLE_BUTTON_GROUP_CHILD but never references it (dead code in toggle-button-group.tsx), and ToggleButton reads ToggleButtonGroupContext unconditionally with no marker gate. So the exact same Tooltip wrap works for ToggleButton.
| Stamps marker on direct children? | Reads context conditionally? | |
|---|---|---|
ButtonGroup + Button |
yes | gated on __button_group_child === true |
ToggleButtonGroup + ToggleButton |
declares marker constant, never uses it | reads context unconditionally |
Your Example Website or App
Reproduces locally on the v3 starter; happy to put together a Stackblitz if helpful.
Steps to Reproduce the Bug or Issue
import { Button, ButtonGroup, Tooltip, ToggleButton, ToggleButtonGroup } from '@heroui/react';
// Works — Button inherits "secondary" variant from ButtonGroup
<ButtonGroup variant="secondary">
<Button>A</Button>
<Button>B</Button>
</ButtonGroup>
// Broken — Buttons render with their own default variant, not "secondary"
<ButtonGroup variant="secondary">
<Tooltip content="Tip A"><Button>A</Button></Tooltip>
<Tooltip content="Tip B"><Button>B</Button></Tooltip>
</ButtonGroup>
// Works — ToggleButton reads its group's context unconditionally
<ToggleButtonGroup>
<Tooltip content="Tip A"><ToggleButton>A</ToggleButton></Tooltip>
<Tooltip content="Tip B"><ToggleButton>B</ToggleButton></Tooltip>
</ToggleButtonGroup>- Render a
ButtonGroupwithvariant="secondary"(or any non-default value). - Wrap each child Button in a
<Tooltip>. - Observe that the Buttons render with the default variant instead of
"secondary". - Swap
Button/ButtonGroupforToggleButton/ToggleButtonGroup— propagation works.
Expected behavior
Wrapping a Button in <Tooltip> (or any other transparent single-element wrapper) shouldn't sever ButtonGroup → Button propagation. The natural fixes are either:
- Have
ButtonconsumeButtonGroupContextunconditionally — mirrorsToggleButton's existing behavior and removes the asymmetry. The risk is that this resurfaces #2142 (Buttons rendered through Modals/Portals from inside a ButtonGroup would inherit group styling, since React context crosses portal boundaries). - Have
ButtonGrouprecurse the marker through known transparent wrappers (Tooltip,Popover.Trigger, etc.) when cloning children — keeps #2142 fixed but requires a wrapper allowlist or a heuristic ("element with a single React element child"). - Have transparent wrappers (
Tooltipetc.) forward unknown props to their first child — standard "transparent forwarding" pattern; handles arbitrary marker props without ButtonGroup needing to know about wrappers.
Screenshots or Videos
n/a
Operating System Version
macOS
Browser
Chrome
Source: heroui-inc/heroui