Skip to content
ShineUI
Install

ButtonGroup

Attached actions with a toolbar feel.

<sui-button-group>ButtonsStable

About

Attached cluster of two to five related actions: one border, shared radius, hairline dividers. By default a role="toolbar" of independent buttons; selectable makes it a persistent single pick, a role="radiogroup" with the brand active fill. It never wraps; a longer set belongs in sui-toolbar. Prefer sui-select-button for the pill track and sui-radio-group to post with a form.

Usage

<sui-button-group aria-label="Document actions"></sui-button-group> <script type="module"> import { defineCustomElements } from "@shineui/core/loader"; import "@shineui/core/shineui.css"; defineCustomElements(); const el = document.querySelector("sui-button-group"); el.items = [ { icon: "fa-solid fa-print", ariaLabel: "Print", value: "print" }, { label: "Export", value: "export" }, { label: "Send", value: "send" }, ]; /* SuiButtonGroupItem[] */ el.addEventListener("suiPick", (e) => console.log(e.detail)); </script>
import { SuiButtonGroup } from "@shineui/react"; import "@shineui/core/shineui.css"; export function Example() { const items = [ { icon: "fa-solid fa-print", ariaLabel: "Print", value: "print" }, { label: "Export", value: "export" }, { label: "Send", value: "send" }, ]; /* SuiButtonGroupItem[] */ return <SuiButtonGroup aria-label="Document actions" items={items} onSuiPick={(e) => console.log(e.detail)} />; }
<script setup> import { SuiButtonGroup } from "@shineui/vue"; import "@shineui/core/shineui.css"; const items = [ { icon: "fa-solid fa-print", ariaLabel: "Print", value: "print" }, { label: "Export", value: "export" }, { label: "Send", value: "send" }, ]; // SuiButtonGroupItem[] const onChange = (e) => console.log(e.detail); </script> <template> <SuiButtonGroup aria-label="Document actions" :items="items" @sui-pick="onChange" /> </template>
import { Component } from "@angular/core"; import { SuiButtonGroup } from "@shineui/angular"; @Component({ standalone: true, imports: [SuiButtonGroup], template: `<sui-button-group aria-label="Document actions" [(active)]="active" [items]="items" (suiPick)="onChange($event)"></sui-button-group>`, }) export class Example { active = -1; items = [ { icon: "fa-solid fa-print", ariaLabel: "Print", value: "print" }, { label: "Export", value: "export" }, { label: "Send", value: "send" }, ]; // SuiButtonGroupItem[] onChange(e: CustomEvent) { console.log(e.detail); } }

Basic

A role="toolbar" of independent actions. aria-label on the element names the cluster; ariaLabel inside an item names that one button, and an icon-only item must carry it, because it is also the tooltip.

<sui-button-group id="bg-actions" aria-label="Document actions"></sui-button-group>
// setup(root): properties and events setup(root) { root.querySelector('#bg-actions').items = documentActions(); }

Selectable

selectable turns the cluster into a role="radiogroup" with one persistent pick; active is the picked index and moves with every press or arrow step.

<sui-button-group id="bg-period" selectable active="0" aria-label="Reporting period"></sui-button-group>
// setup(root): properties and events setup(root) { root.querySelector('#bg-period').items = periods(); }

Vertical

orientation="vertical" stacks the items and sets aria-orientation, so a toolbar answers Up and Down instead of Left and Right.

<div style="display:flex;gap:16px;align-items:flex-start"> <sui-button-group id="bg-v-actions" orientation="vertical" aria-label="Row actions"></sui-button-group> <sui-button-group id="bg-v-period" orientation="vertical" selectable active="0" aria-label="Reporting period"></sui-button-group> </div>
// setup(root): properties and events setup(root) { root.querySelector('#bg-v-actions').items = documentActions(); root.querySelector('#bg-v-period').items = periods(); }

Sizes

size sets every item in the cluster at once, at the same 28, 34 and 42px heights the rest of the control family uses.

<div style="display:flex;flex-direction:column;gap:12px;align-items:flex-start"> <sui-button-group id="bg-sm" size="sm" aria-label="Document actions"></sui-button-group> <sui-button-group id="bg-md" aria-label="Document actions"></sui-button-group> <sui-button-group id="bg-lg" size="lg" aria-label="Document actions"></sui-button-group> </div>
// setup(root): properties and events setup(root) { for (const id of ['#bg-sm', '#bg-md', '#bg-lg']) root.querySelector(id).items = documentActions(); }

Disabled

A disabled item is skipped by the arrows and cannot be pressed; a disabled item that is the current pick keeps its fill, like a disabled native radio. disabled on the group removes every item from the tab order.

<div style="display:flex;flex-direction:column;gap:12px;align-items:flex-start"> <sui-button-group id="bg-item-off" aria-label="Row actions"></sui-button-group> <sui-button-group id="bg-pick-off" selectable active="1" aria-label="Delivery speed"></sui-button-group> <sui-button-group id="bg-group-off" disabled aria-label="Document actions"></sui-button-group> </div>
// setup(root): properties and events setup(root) { root.querySelector('#bg-item-off').items = [{ label: 'Edit' }, { label: 'Duplicate' }, { label: 'Archive', disabled: true }]; root.querySelector('#bg-pick-off').items = [{ label: 'Standard' }, { label: 'Express', disabled: true }, { label: 'Overnight' }]; root.querySelector('#bg-group-off').items = documentActions(); }

Events

suiPick fires on every press with the item, and item.command() runs. suiActiveChange fires in selectable mode when the pick moves, with the new index. It reports the state of the group itself, so it does not bubble.

Nothing picked yet.
<div style="display:flex;flex-direction:column;gap:12px;align-items:flex-start"> <sui-button-group id="bg-live-actions" aria-label="Document actions"></sui-button-group> <sui-button-group id="bg-live-period" selectable active="0" aria-label="Reporting period"></sui-button-group> <span id="bg-log" style="font:460 13px/1.6 var(--font-sans);color:var(--ink-muted)">Nothing picked yet.</span> </div>
// setup(root): properties and events setup(root) { const log = root.querySelector('#bg-log'); const actions = root.querySelector('#bg-live-actions'); const period = root.querySelector('#bg-live-period'); actions.items = documentActions(); period.items = periods(); actions.addEventListener('suiPick', (e) => { log.textContent = `Picked ${e.detail.ariaLabel || e.detail.label} (${e.detail.value}).`; }); period.addEventListener('suiPick', (e) => { log.textContent = `Picked ${e.detail.label} (${e.detail.value}).`; }); period.addEventListener('suiActiveChange', (e) => { log.textContent = `Period is now index ${e.detail}.`; }); }

Keyboard

KeyAction
TabMoves focus into the group, which is one tab stop: the picked item in selectable mode, otherwise the first enabled item.
ArrowRight ArrowLeftMove focus to the next or previous enabled item in a horizontal group, wrapping at the ends. In selectable mode the focused item is also picked.
ArrowDown ArrowUpThe same in a vertical group. A selectable group answers all four arrows whatever its orientation.
Home EndMove focus to the first or last enabled item, and pick it in selectable mode.
Enter SpacePress the focused item.

API

<sui-button-group>

Properties

PropertyAttributeTypeDefaultDescription
activeactivenumber-1Index of the picked item when selectable; -1 for none. Mutable: updated on press and reported through suiActiveChange.
disableddisabledbooleanfalseWhen true, every item is non-interactive: no tab stop and no activation.
itemsproperty onlySuiButtonGroupItem[][]Buttons: { label, icon, ariaLabel, disabled, value, command }[]. Property, not attribute. ariaLabel is required on an icon-only item: it is the button's only accessible name and it becomes the tooltip. Items with a visible label get no tooltip. An aria-label on the element itself names the whole cluster.
orientationorientation"horizontal" | "vertical"'horizontal'Cluster axis. Vertical stacks the items and sets aria-orientation="vertical".
selectableselectablebooleanfalsePersistent single pick (role="radiogroup", aria-checked radios) instead of independent actions. Moving focus with the arrows also moves the pick, like native radios, so every arrow step is a selection: keep item.command() cheap.
sizesize"lg" | "md" | "sm"'md'Button height: sm 28px, md 34px (toolbars), lg 42px. Reflected.

Events

EventDetailReactVueDescription
suiActiveChangenumberonSuiActiveChange@sui-active-changeFires when the pick moves (selectable only); detail is the new active index. A state report, not a field's value, so a form around the group never hears it. Does not bubble.
suiPickSuiButtonGroupItemonSuiPick@sui-pickFires on every press of an enabled item; detail is the item. item.command() also runs.

Methods

MethodSignatureDescription
setFocus()setFocus() => Promise<void>Moves keyboard focus to the current tab-stop item (active if selectable and enabled, else the first enabled).

Types

SuiButtonGroupItem interfaceOne attached button. Icon-only items MUST carry ariaLabel; it also becomes the tooltip.
label?stringVisible text. Omit for an icon-only button.
icon?stringFont Awesome class, e.g. "fa-solid fa-print".
ariaLabel?stringAccessible name; required when there is no label. Shown as a sui-tooltip on icon-only items.
disabled?boolean
value?stringStable pick id. Optional; when set, suiPick detail includes it so consumers can identify the item without relying on index.
command?() => voidRuns on selection: a press, or an arrow/Home/End step in selectable mode (in addition to the suiPick event). Keep it cheap and safe to run twice.

Theming

Every design token this element can render with, read from its own stylesheets and the ones it imports. Set a token on :root or on any ancestor and every use below follows it. The values shown are the ones in the theme you are viewing. The list is read per stylesheet, so an element that imports a shared drawing module lists that module's tokens too, whether or not it paints with all of them.

See also