---
outline: deep
---

# Button group

A wrapper for multiple related buttons, joined into a single unit with shared borders. Buttons keep their own state; the group never manages the selection — for a single choice, see [segmented control](./segmented-control.md) when it applies immediately, or [radio group](./radio-group.md) when the value is submitted with a form.

**`<l-button-group>`** — Custom Element (no Shadow DOM)

```html
<l-button-group label="Record actions">
  <button class="l-button">Edit</button>
  <button class="l-button">Duplicate</button>
  <button class="l-button">Archive</button>
</l-button-group>
```

## Options

### Label

Add `label` to give the group an accessible name announced by screen readers.

```html
<l-button-group label="Record actions">
  <button class="l-button">Edit</button>
  <button class="l-button">Duplicate</button>
  <button class="l-button">Archive</button>
</l-button-group>
```

### Orientation

Add `orientation="vertical"` to stack the buttons.

```html
<l-button-group
  label="Record actions"
  orientation="vertical"
>
  <button class="l-button">Edit</button>
  <button class="l-button">Duplicate</button>
  <button class="l-button">Archive</button>
</l-button-group>
```

### Sizes

Set `data-size` on every button of a group so it stays even. The sizes below are `sm`, the default `md`, `lg`, and `xl`.

```html
<l-button-group label="Pagination, small">
  <button
    class="l-button"
    data-size="sm"
  >
    Previous
  </button>
  <button
    class="l-button"
    data-size="sm"
  >
    Next
  </button>
</l-button-group>

<l-button-group label="Pagination, medium">
  <button class="l-button">Previous</button>
  <button class="l-button">Next</button>
</l-button-group>

<l-button-group label="Pagination, large">
  <button
    class="l-button"
    data-size="lg"
  >
    Previous
  </button>
  <button
    class="l-button"
    data-size="lg"
  >
    Next
  </button>
</l-button-group>

<l-button-group label="Pagination, extra large">
  <button
    class="l-button"
    data-size="xl"
  >
    Previous
  </button>
  <button
    class="l-button"
    data-size="xl"
  >
    Next
  </button>
</l-button-group>
```

## Examples

### Toolbar

Sit several groups side by side to build a toolbar. Each group keeps its own `label`, so assistive tech announces which set a button belongs to, and each group decides whether it carries state:

- **History** — plain actions, no state.
- **Text formatting** — independent toggles; several can be pressed at once.
- **Text alignment** — a single choice; exactly one stays pressed.

```html
<div class="flex flex-wrap items-center gap-2">
  <l-button-group label="History">
    <button
      class="l-button"
      data-icon-only
      aria-label="Undo"
    >
      <l-icon name="lucide:undo-2"></l-icon>
    </button>
    <button
      class="l-button"
      data-icon-only
      aria-label="Redo"
    >
      <l-icon name="lucide:redo-2"></l-icon>
    </button>
  </l-button-group>

  <l-button-group label="Text formatting">
    <button
      class="l-button"
      data-icon-only
      aria-label="Bold"
      aria-pressed="true"
    >
      <l-icon name="lucide:bold"></l-icon>
    </button>
    <button
      class="l-button"
      data-icon-only
      aria-label="Italic"
      aria-pressed="true"
    >
      <l-icon name="lucide:italic"></l-icon>
    </button>
    <button
      class="l-button"
      data-icon-only
      aria-label="Underline"
      aria-pressed="false"
    >
      <l-icon name="lucide:underline"></l-icon>
    </button>
  </l-button-group>

  <l-button-group label="Text alignment">
    <button
      class="l-button"
      data-icon-only
      aria-label="Align left"
      aria-pressed="true"
    >
      <l-icon name="lucide:align-left"></l-icon>
    </button>
    <button
      class="l-button"
      data-icon-only
      aria-label="Align center"
      aria-pressed="false"
    >
      <l-icon name="lucide:align-center"></l-icon>
    </button>
    <button
      class="l-button"
      data-icon-only
      aria-label="Align right"
      aria-pressed="false"
    >
      <l-icon name="lucide:align-right"></l-icon>
    </button>
  </l-button-group>
</div>
```

Buttons that carry state expose it with `aria-pressed`, and a pressed button keeps its active fill in every variant. The group never manages the selection — your application owns it. Independent toggles flip their own state:

```js
formatting.addEventListener('click', (event) => {
  const button = event.target.closest('button');
  if (!button) return;

  const pressed = button.getAttribute('aria-pressed') === 'true';
  button.setAttribute('aria-pressed', String(!pressed));
});
```

A single choice presses one button and releases the others:

```js
alignment.addEventListener('click', (event) => {
  const button = event.target.closest('button');
  if (!button) return;

  for (const b of alignment.querySelectorAll('button')) {
    b.setAttribute('aria-pressed', String(b === button));
  }
});
```

Add `data-icon-only` to a button with no visible text to square it, and give it an `aria-label`.

The wrapper is a plain element, not `role="toolbar"`: that role expects arrow-key navigation across the whole bar, which the groups do not implement — each button stays a `Tab` stop.

For a single choice, reach for [segmented control](./segmented-control.md) when it applies immediately, or [radio group](./radio-group.md) when the value is submitted with a form.

### Split button

Pair an action with an `<l-dropdown>` of related ones. The group rounds the corners of the dropdown trigger automatically.

```html
<l-button-group label="Save options">
  <button class="l-button">Save</button>
  <l-dropdown>
    <button
      slot="trigger"
      class="l-button"
      data-icon-only
      aria-label="More save options"
    >
      <l-icon name="lucide:chevron-down"></l-icon>
    </button>
    <l-dropdown-item value="draft">Save as draft</l-dropdown-item>
    <l-dropdown-item value="template">Save as template</l-dropdown-item>
  </l-dropdown>
</l-button-group>
```

## Accessibility

### Criteria

- **Role** — Host gets `role="group"` to expose the buttons as a set
- **Accessible name** — `label` sets `aria-label` so assistive tech announces the group purpose
- **Orientation** — `orientation` is visual only. ARIA 1.2 does not allow `aria-orientation` on `role="group"`, so no ARIA attribute is set
- **Focus order** — Each button is an independent `Tab` stop; the focus ring is raised above neighbours so it is never clipped
- **Toggle state** — Buttons that carry state expose it with `aria-pressed`; the group never sets it
- **Icon-only buttons** — Each icon-only button needs its own `aria-label`

### Rules

- Always add a `label` describing the group purpose
- Children must be native `<button class="l-button">` elements (optionally wrapped in `l-dropdown` for split buttons)
- Give each icon-only button its own `aria-label`
- For toggle buttons, set `aria-pressed` on every button of the group, not only the pressed one

### Keyboard interactions

- `Tab` — Moves focus to the next button in the group
- `Shift + Tab` — Moves focus to the previous button
- `Enter` — Activates the focused button
- `Space` — Activates the focused button

## API reference

### Importing

```js
import 'luxen-ui/button-group';
```

```css
@import 'luxen-ui/css/button-group';
```

### Attributes & Properties

- **label**: `string | undefined` — Accessible label announced for the group. Not displayed on screen.
- **orientation**: `ButtonGroupOrientation` (default: `'horizontal'`) — Layout direction of the buttons. Visual only (drives the CSS via the
reflected attribute): ARIA 1.2 does not allow `aria-orientation` on
`role="group"`, so no ARIA attribute is set.
