Skip to content

Button group <l-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 when it applies immediately, or radio group when the value is submitted with a form.

HTML tag<l-button-group>
Native
Progressive
Plain
Shadow-DOM
Plain Custom Element
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.

Code
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.

Code
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.

Code
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.

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 when it applies immediately, or radio group when the value is submitted with a form.

Code
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>

Split button

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

Save as draft Save as template
Code
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

WCAG1.3.1
RGAA9.3
Accessible name

label sets aria-label so assistive tech announces the group purpose

WCAG4.1.2
RGAA11.1
Orientation

orientation is visual only. ARIA 1.2 does not allow aria-orientation on role="group", so no ARIA attribute is set

WCAG1.3.1
Focus order

Each button is an independent Tab stop; the focus ring is raised above neighbours so it is never clipped

RGAA10.7
Toggle state

Buttons that carry state expose it with aria-pressed; the group never sets it

WCAG4.1.2
RGAA11.1
Icon-only buttons

Each icon-only button needs its own aria-label

WCAG4.1.2
RGAA11.1

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
ShiftTab
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

labelstring | undefinedProperty
Accessible label announced for the group. Not displayed on screen.
orientationButtonGroupOrientationdefault:'horizontal'Property
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.