Button group
Join adjacent buttons into a single segmented unit, horizontal or vertical.
Wrap two or more .buttons in .button-group to fuse them into one segmented
control. The group squares off the inner corners and collapses the shared border so
the buttons read as a unit, while each button keeps its own data-variant and
data-size. Add role="group" and an aria-label so assistive tech announces the
set.
Horizontal
The default orientation lays buttons out in a row.
<div class="flex flex-col items-start gap-md"> <div class="button-group" role="group" aria-label="Text alignment"> <button class="button" type="button">Left</button> <button class="button" type="button">Center</button> <button class="button" type="button">Right</button> </div> <div class="button-group" role="group" aria-label="Pagination"> <button class="button" data-variant="muted" type="button">Previous</button> <button class="button" data-variant="muted" type="button">1</button> <button class="button" data-variant="muted" type="button">2</button> <button class="button" data-variant="muted" type="button">3</button> <button class="button" data-variant="muted" type="button">Next</button> </div> <div class="button-group" role="group" aria-label="Formatting"> <button class="button" data-size="icon" type="button" aria-label="Bold"> <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" > <path d="M6 4h8a4 4 0 0 1 0 8H6z" /> <path d="M6 12h9a4 4 0 0 1 0 8H6z" /> </svg> </button> <button class="button" data-size="icon" type="button" aria-label="Italic"> <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" > <line x1="19" y1="4" x2="10" y2="4" /> <line x1="14" y1="20" x2="5" y2="20" /> <line x1="15" y1="4" x2="9" y2="20" /> </svg> </button> <button class="button" data-size="icon" type="button" aria-label="Underline"> <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" > <path d="M6 4v6a6 6 0 0 0 12 0V4" /> <line x1="4" y1="20" x2="20" y2="20" /> </svg> </button> </div></div>/** * _button-group.css — Button group (.button-group) * * @layer components * @requires layers.css, _variables.css, _button.css * @uses :not() — first/last edge selection for shared-border collapse * @uses logical border-radius longhands — RTL-safe corner rounding per orientation * @uses flex-item z-index — raise the active control so its border/ring isn't clipped * @see https://ui.shadcn.com/docs/components/base/button-group */@layer zazz.components { /* =========================================================================== BUTTON GROUP Joins adjacent controls (.button, and any child it wraps) into a single segmented unit. Children keep their own --button-radius, so the outer corners follow the buttons' size variant automatically — only the *inner* corners are squared off and the doubled border is collapsed with a -1px overlap. =========================================================================== */ .button-group { display: flex; inline-size: fit-content; /* stretch so mixed-height children share one edge */ align-items: stretch; border-radius: var(--button-radius); overflow: clip; background-color: var(--background); position: relative; isolation: isolate; } /* flex items honour z-index without positioning, but relative keeps the stacking context robust across child types (inputs, selects, …) */ .button-group > * { position: relative; } /* the focused/hovered/pressed control floats above its neighbours so its border and box-shadow ring paint over the overlapped edge */ .button-group > *:hover:not(:disabled, [aria-disabled="true"]), .button-group > *:focus-visible, .button-group > *:active:not(:disabled, [aria-disabled="true"]) { z-index: 1; } /* =========================================================================== BUTTON GROUP — HORIZONTAL (default) =========================================================================== */ .button-group:not([data-orientation="vertical"]) { flex-direction: row; } .button-group:not([data-orientation="vertical"]) > :not(:last-child) { border-start-end-radius: 0; border-end-end-radius: 0; } .button-group:not([data-orientation="vertical"]) > :not(:first-child) { /* pull onto the previous control's border so the seam is one line, not two */ margin-inline-start: -1px; border-start-start-radius: 0; border-end-start-radius: 0; } /* =========================================================================== BUTTON GROUP — VERTICAL =========================================================================== */ .button-group[data-orientation="vertical"] { flex-direction: column; } .button-group[data-orientation="vertical"] > :not(:last-child) { border-end-start-radius: 0; border-end-end-radius: 0; } .button-group[data-orientation="vertical"] > :not(:first-child) { margin-block-start: -1px; border-start-start-radius: 0; border-start-end-radius: 0; }}Vertical
Set data-orientation="vertical" on the wrapper to stack them.
<div class="button-group" data-orientation="vertical" role="group" aria-label="View options"> <button class="button" type="button">List</button> <button class="button" type="button">Board</button> <button class="button" type="button">Calendar</button> <button class="button" type="button">Timeline</button></div>/** * _button-group.css — Button group (.button-group) * * @layer components * @requires layers.css, _variables.css, _button.css * @uses :not() — first/last edge selection for shared-border collapse * @uses logical border-radius longhands — RTL-safe corner rounding per orientation * @uses flex-item z-index — raise the active control so its border/ring isn't clipped * @see https://ui.shadcn.com/docs/components/base/button-group */@layer zazz.components { /* =========================================================================== BUTTON GROUP Joins adjacent controls (.button, and any child it wraps) into a single segmented unit. Children keep their own --button-radius, so the outer corners follow the buttons' size variant automatically — only the *inner* corners are squared off and the doubled border is collapsed with a -1px overlap. =========================================================================== */ .button-group { display: flex; inline-size: fit-content; /* stretch so mixed-height children share one edge */ align-items: stretch; border-radius: var(--button-radius); overflow: clip; background-color: var(--background); position: relative; isolation: isolate; } /* flex items honour z-index without positioning, but relative keeps the stacking context robust across child types (inputs, selects, …) */ .button-group > * { position: relative; } /* the focused/hovered/pressed control floats above its neighbours so its border and box-shadow ring paint over the overlapped edge */ .button-group > *:hover:not(:disabled, [aria-disabled="true"]), .button-group > *:focus-visible, .button-group > *:active:not(:disabled, [aria-disabled="true"]) { z-index: 1; } /* =========================================================================== BUTTON GROUP — HORIZONTAL (default) =========================================================================== */ .button-group:not([data-orientation="vertical"]) { flex-direction: row; } .button-group:not([data-orientation="vertical"]) > :not(:last-child) { border-start-end-radius: 0; border-end-end-radius: 0; } .button-group:not([data-orientation="vertical"]) > :not(:first-child) { /* pull onto the previous control's border so the seam is one line, not two */ margin-inline-start: -1px; border-start-start-radius: 0; border-end-start-radius: 0; } /* =========================================================================== BUTTON GROUP — VERTICAL =========================================================================== */ .button-group[data-orientation="vertical"] { flex-direction: column; } .button-group[data-orientation="vertical"] > :not(:last-child) { border-end-start-radius: 0; border-end-end-radius: 0; } .button-group[data-orientation="vertical"] > :not(:first-child) { margin-block-start: -1px; border-start-start-radius: 0; border-start-end-radius: 0; }}API
| Attribute | Values |
|---|---|
data-orientation | vertical (omit = horizontal default) |
The group reads each child's own --button-radius, so the outer corners follow the
buttons' data-size automatically — only the inner corners are squared. Need a
divider between segments? Drop your own separator element in as a child; it inherits
the same border collapse. You never need to touch zazz/styles/.