Skip to content

Type to search the docs, API, and blog.

↑↓ navigate↵ openesc close
Zazz 0.7 alpha
API navigation

API

Button group

Buttons joined into one control with shared borders.

Installation

pnpm dlx zazz-ui add button-group
npx zazz-ui add button-group
yarn dlx zazz-ui add button-group
bunx zazz-ui add button-group

Also adds fields, kbd, button, which it depends on. Run zazz-ui init first if the project has no zazz/ folder yet.

Examples

Default

<div
  style="
    --display: flex-col;
    --items: start;
    --gap: 6;
  "
>
  <ui-button-group role="group" aria-label="Text alignment">
    <button data-ui="button" type="button">Left</button>
    <button data-ui="button" type="button">Center</button>
    <button data-ui="button" type="button">Right</button>
  </ui-button-group>

  <ui-button-group role="group" aria-label="Pagination">
    <button data-ui="button" data-button-variant="muted" type="button">Previous</button>
    <button data-ui="button" data-button-variant="muted" type="button">1</button>
    <button data-ui="button" data-button-variant="muted" type="button">2</button>
    <button data-ui="button" data-button-variant="muted" type="button">3</button>
    <button data-ui="button" data-button-variant="muted" type="button">Next</button>
  </ui-button-group>

  <ui-button-group role="group" aria-label="Formatting">
    <button data-ui="button" data-button-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 data-ui="button" data-button-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 data-ui="button" data-button-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>
  </ui-button-group>
</div>
/**
 * button-group.css: Button group (ui-button-group | [data-ui~="button-group"])
 *
 * @layer      variables, components
 * @requires   _layers.css, _variables.css, _properties.css (--_*-resolved), 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
 * @tokens     --ui-button-group-* (@layer variables; named after the utilities they back)
 */

@layer variables {
  :root {
    --ui-button-group-rounded: var(--ui-button-rounded);
    --ui-button-group-bg: var(--color-card);
  }
}

@property --_button-group-rounded {
  syntax: "*";
  inherits: false;
}

@property --_button-group-bg {
  syntax: "*";
  inherits: false;
}

@layer ui.components {
  /* ===========================================================================
  BUTTON GROUP

  Joins adjacent controls (buttons, and any child it wraps) into a single
  segmented unit. Children keep their own --ui-button-rounded, so the outer corners
  follow the buttons' size preset automatically; only the *inner* corners are
  squared off and the doubled border is collapsed by dropping
  each later control's leading border (no overlap).
  =========================================================================== */

  :where(ui-button-group, [data-ui~="button-group"]) {
    display: flex;
    inline-size: fit-content;
    /* stretch so mixed-height children share one edge */
    align-items: stretch;
    border-radius: var(
      --_rounded-resolved,
      var(--_button-group-rounded, var(--ui-button-group-rounded))
    );
    background-color: var(--_bg-resolved, var(--_button-group-bg, var(--ui-button-group-bg)));
    position: relative;
    isolation: isolate;
  }

  /* flex items honour z-index without positioning, but relative keeps the
     stacking context robust across child types (inputs, selects, …) */
  :where(ui-button-group, [data-ui~="button-group"]) > * {
    position: relative;
  }

  /* the focused/hovered/pressed control floats above its neighbors so its
     focus ring paints over its neighbors */
  :where(ui-button-group, [data-ui~="button-group"])
    > *:hover:not(:disabled, [aria-disabled="true"]),
  :where(ui-button-group, [data-ui~="button-group"]) > *:focus-visible,
  :where(ui-button-group, [data-ui~="button-group"])
    > *:active:not(:disabled, [aria-disabled="true"]) {
    z-index: 1;
  }

  /* ===========================================================================
  BUTTON GROUP: HORIZONTAL (default)
  =========================================================================== */

  :where(ui-button-group, [data-ui~="button-group"]):not(
    [data-button-group-orientation="vertical"]
  ) {
    flex-direction: row;
  }

  :where(ui-button-group, [data-ui~="button-group"]):not([data-button-group-orientation="vertical"])
    > :not(:last-child) {
    border-start-end-radius: 0;
    border-end-end-radius: 0;
  }

  :where(ui-button-group, [data-ui~="button-group"]):not([data-button-group-orientation="vertical"])
    > :not(:first-child) {
    /* drop the leading border so neighbors share one seam, not two */
    border-inline-start-width: 0;
    border-start-start-radius: 0;
    border-end-start-radius: 0;
  }

  /* ===========================================================================
  BUTTON GROUP: VERTICAL — data-button-group-orientation="vertical"
  =========================================================================== */

  :where(
    ui-button-group[data-button-group-orientation="vertical"],
    [data-ui~="button-group"][data-button-group-orientation="vertical"]
  ) {
    flex-direction: column;
  }

  :where(
      ui-button-group[data-button-group-orientation="vertical"],
      [data-ui~="button-group"][data-button-group-orientation="vertical"]
    )
    > :not(:last-child) {
    border-end-start-radius: 0;
    border-end-end-radius: 0;
  }

  :where(
      ui-button-group[data-button-group-orientation="vertical"],
      [data-ui~="button-group"][data-button-group-orientation="vertical"]
    )
    > :not(:first-child) {
    /* drop the leading border so neighbors share one seam, not two */
    border-block-start-width: 0;
    border-start-start-radius: 0;
    border-start-end-radius: 0;
  }
}

Vertical

<ui-button-group data-button-group-orientation="vertical" role="group" aria-label="View options">
  <button data-ui="button" type="button">List</button>
  <button data-ui="button" type="button">Board</button>
  <button data-ui="button" type="button">Calendar</button>
  <button data-ui="button" type="button">Timeline</button>
</ui-button-group>
/**
 * button-group.css: Button group (ui-button-group | [data-ui~="button-group"])
 *
 * @layer      variables, components
 * @requires   _layers.css, _variables.css, _properties.css (--_*-resolved), 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
 * @tokens     --ui-button-group-* (@layer variables; named after the utilities they back)
 */

@layer variables {
  :root {
    --ui-button-group-rounded: var(--ui-button-rounded);
    --ui-button-group-bg: var(--color-card);
  }
}

@property --_button-group-rounded {
  syntax: "*";
  inherits: false;
}

@property --_button-group-bg {
  syntax: "*";
  inherits: false;
}

@layer ui.components {
  /* ===========================================================================
  BUTTON GROUP

  Joins adjacent controls (buttons, and any child it wraps) into a single
  segmented unit. Children keep their own --ui-button-rounded, so the outer corners
  follow the buttons' size preset automatically; only the *inner* corners are
  squared off and the doubled border is collapsed by dropping
  each later control's leading border (no overlap).
  =========================================================================== */

  :where(ui-button-group, [data-ui~="button-group"]) {
    display: flex;
    inline-size: fit-content;
    /* stretch so mixed-height children share one edge */
    align-items: stretch;
    border-radius: var(
      --_rounded-resolved,
      var(--_button-group-rounded, var(--ui-button-group-rounded))
    );
    background-color: var(--_bg-resolved, var(--_button-group-bg, var(--ui-button-group-bg)));
    position: relative;
    isolation: isolate;
  }

  /* flex items honour z-index without positioning, but relative keeps the
     stacking context robust across child types (inputs, selects, …) */
  :where(ui-button-group, [data-ui~="button-group"]) > * {
    position: relative;
  }

  /* the focused/hovered/pressed control floats above its neighbors so its
     focus ring paints over its neighbors */
  :where(ui-button-group, [data-ui~="button-group"])
    > *:hover:not(:disabled, [aria-disabled="true"]),
  :where(ui-button-group, [data-ui~="button-group"]) > *:focus-visible,
  :where(ui-button-group, [data-ui~="button-group"])
    > *:active:not(:disabled, [aria-disabled="true"]) {
    z-index: 1;
  }

  /* ===========================================================================
  BUTTON GROUP: HORIZONTAL (default)
  =========================================================================== */

  :where(ui-button-group, [data-ui~="button-group"]):not(
    [data-button-group-orientation="vertical"]
  ) {
    flex-direction: row;
  }

  :where(ui-button-group, [data-ui~="button-group"]):not([data-button-group-orientation="vertical"])
    > :not(:last-child) {
    border-start-end-radius: 0;
    border-end-end-radius: 0;
  }

  :where(ui-button-group, [data-ui~="button-group"]):not([data-button-group-orientation="vertical"])
    > :not(:first-child) {
    /* drop the leading border so neighbors share one seam, not two */
    border-inline-start-width: 0;
    border-start-start-radius: 0;
    border-end-start-radius: 0;
  }

  /* ===========================================================================
  BUTTON GROUP: VERTICAL — data-button-group-orientation="vertical"
  =========================================================================== */

  :where(
    ui-button-group[data-button-group-orientation="vertical"],
    [data-ui~="button-group"][data-button-group-orientation="vertical"]
  ) {
    flex-direction: column;
  }

  :where(
      ui-button-group[data-button-group-orientation="vertical"],
      [data-ui~="button-group"][data-button-group-orientation="vertical"]
    )
    > :not(:last-child) {
    border-end-start-radius: 0;
    border-end-end-radius: 0;
  }

  :where(
      ui-button-group[data-button-group-orientation="vertical"],
      [data-ui~="button-group"][data-button-group-orientation="vertical"]
    )
    > :not(:first-child) {
    /* drop the leading border so neighbors share one seam, not two */
    border-block-start-width: 0;
    border-start-start-radius: 0;
    border-start-end-radius: 0;
  }
}

Attributes

button-group attributes
AttributeKindValuesNotes
data-ui="button-group"Identityfree valueIdentity token in data-ui.
<ui-button-group>Tag formfree valueTag form; styled and, when scripted, registered under this name.
data-button-group-orientationPresetverticalPreset: a named bundle of values.

Requires: button.

Hooks

Set a hook on :root, on a subtree, or inline on one element. A state hook (--…--hover) themes that state only; the primitive keeps its other states.

button-group hooks
HookDefaultState
--ui-button-group-bgvar(--color-card)—
--ui-button-group-roundedvar(--ui-button-rounded)—