Skip to content

Type to search the docs, API, and blog.

↑↓ navigate↵ openesc close
Zazz 0.7 alpha
API navigation

API

Toggle group

Toggles joined into one control, single or multiple selection.

Installation

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

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

Examples

Default

<ui-toggle-group role="group" aria-label="Text alignment">
  <label data-ui="toggle" data-toggle-size="icon">
    <input type="radio" name="align" aria-label="Align left" checked />
    <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="4" y1="6" x2="20" y2="6" />
      <line x1="4" y1="12" x2="14" y2="12" />
      <line x1="4" y1="18" x2="18" y2="18" />
    </svg>
  </label>
  <label data-ui="toggle" data-toggle-size="icon">
    <input type="radio" name="align" aria-label="Align center" />
    <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="4" y1="6" x2="20" y2="6" />
      <line x1="7" y1="12" x2="17" y2="12" />
      <line x1="5" y1="18" x2="19" y2="18" />
    </svg>
  </label>
  <label data-ui="toggle" data-toggle-size="icon">
    <input type="radio" name="align" aria-label="Align right" />
    <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="4" y1="6" x2="20" y2="6" />
      <line x1="10" y1="12" x2="20" y2="12" />
      <line x1="6" y1="18" x2="20" y2="18" />
    </svg>
  </label>
</ui-toggle-group>
/**
 * toggle-group.css: Toggle group (ui-toggle-group | [data-ui~="toggle-group"])
 *
 * @layer      variables, components
 * @requires   _layers.css, _variables.css, _properties.css (--_*-resolved), toggle.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-toggle-group-* (@layer variables; named after the utilities they back)
 */

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

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

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

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

  Joins adjacent toggles into a single segmented unit: a single-select set
  (radios sharing a `name`) or a multi-select set (checkboxes). Children keep
  their own --ui-toggle-rounded, so the outer corners follow the toggles' 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-toggle-group, [data-ui~="toggle-group"]) {
    display: flex;
    inline-size: fit-content;
    /* stretch so mixed-height children share one edge */
    align-items: stretch;
    border-radius: var(
      --_rounded-resolved,
      var(--_toggle-group-rounded, var(--ui-toggle-group-rounded))
    );
    background-color: var(--_bg-resolved, var(--_toggle-group-bg, var(--ui-toggle-group-bg)));
    position: relative;
    isolation: isolate;
  }

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

  /* the focused/hovered/checked toggle floats above its neighbors so its
     focus ring paints over its neighbors */
  :where(ui-toggle-group, [data-ui~="toggle-group"]) > *:hover:not(:has(> input:disabled)),
  :where(ui-toggle-group, [data-ui~="toggle-group"]) > *:focus-within,
  :where(ui-toggle-group, [data-ui~="toggle-group"]) > *:has(> input:checked) {
    z-index: 1;
  }

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

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

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

  :where(ui-toggle-group, [data-ui~="toggle-group"]):not([data-toggle-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;
  }

  /* ===========================================================================
  TOGGLE GROUP: VERTICAL — data-toggle-group-orientation="vertical"
  =========================================================================== */

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

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

  :where(
      ui-toggle-group[data-toggle-group-orientation="vertical"],
      [data-ui~="toggle-group"][data-toggle-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;
  }
}

Multiple

<ui-toggle-group role="group" aria-label="Text formatting">
  <label data-ui="toggle">
    <input type="checkbox" name="format" value="bold" checked />
    <span>Bold</span>
  </label>
  <label data-ui="toggle">
    <input type="checkbox" name="format" value="italic" />
    <span>Italic</span>
  </label>
  <label data-ui="toggle">
    <input type="checkbox" name="format" value="underline" checked />
    <span>Underline</span>
  </label>
</ui-toggle-group>
/**
 * toggle-group.css: Toggle group (ui-toggle-group | [data-ui~="toggle-group"])
 *
 * @layer      variables, components
 * @requires   _layers.css, _variables.css, _properties.css (--_*-resolved), toggle.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-toggle-group-* (@layer variables; named after the utilities they back)
 */

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

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

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

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

  Joins adjacent toggles into a single segmented unit: a single-select set
  (radios sharing a `name`) or a multi-select set (checkboxes). Children keep
  their own --ui-toggle-rounded, so the outer corners follow the toggles' 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-toggle-group, [data-ui~="toggle-group"]) {
    display: flex;
    inline-size: fit-content;
    /* stretch so mixed-height children share one edge */
    align-items: stretch;
    border-radius: var(
      --_rounded-resolved,
      var(--_toggle-group-rounded, var(--ui-toggle-group-rounded))
    );
    background-color: var(--_bg-resolved, var(--_toggle-group-bg, var(--ui-toggle-group-bg)));
    position: relative;
    isolation: isolate;
  }

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

  /* the focused/hovered/checked toggle floats above its neighbors so its
     focus ring paints over its neighbors */
  :where(ui-toggle-group, [data-ui~="toggle-group"]) > *:hover:not(:has(> input:disabled)),
  :where(ui-toggle-group, [data-ui~="toggle-group"]) > *:focus-within,
  :where(ui-toggle-group, [data-ui~="toggle-group"]) > *:has(> input:checked) {
    z-index: 1;
  }

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

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

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

  :where(ui-toggle-group, [data-ui~="toggle-group"]):not([data-toggle-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;
  }

  /* ===========================================================================
  TOGGLE GROUP: VERTICAL — data-toggle-group-orientation="vertical"
  =========================================================================== */

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

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

  :where(
      ui-toggle-group[data-toggle-group-orientation="vertical"],
      [data-ui~="toggle-group"][data-toggle-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-toggle-group data-toggle-group-orientation="vertical" role="group" aria-label="Map layer">
  <label data-ui="toggle">
    <input type="radio" name="layer" value="map" checked />
    <span>Map</span>
  </label>
  <label data-ui="toggle">
    <input type="radio" name="layer" value="satellite" />
    <span>Satellite</span>
  </label>
  <label data-ui="toggle">
    <input type="radio" name="layer" value="terrain" />
    <span>Terrain</span>
  </label>
</ui-toggle-group>
/**
 * toggle-group.css: Toggle group (ui-toggle-group | [data-ui~="toggle-group"])
 *
 * @layer      variables, components
 * @requires   _layers.css, _variables.css, _properties.css (--_*-resolved), toggle.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-toggle-group-* (@layer variables; named after the utilities they back)
 */

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

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

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

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

  Joins adjacent toggles into a single segmented unit: a single-select set
  (radios sharing a `name`) or a multi-select set (checkboxes). Children keep
  their own --ui-toggle-rounded, so the outer corners follow the toggles' 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-toggle-group, [data-ui~="toggle-group"]) {
    display: flex;
    inline-size: fit-content;
    /* stretch so mixed-height children share one edge */
    align-items: stretch;
    border-radius: var(
      --_rounded-resolved,
      var(--_toggle-group-rounded, var(--ui-toggle-group-rounded))
    );
    background-color: var(--_bg-resolved, var(--_toggle-group-bg, var(--ui-toggle-group-bg)));
    position: relative;
    isolation: isolate;
  }

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

  /* the focused/hovered/checked toggle floats above its neighbors so its
     focus ring paints over its neighbors */
  :where(ui-toggle-group, [data-ui~="toggle-group"]) > *:hover:not(:has(> input:disabled)),
  :where(ui-toggle-group, [data-ui~="toggle-group"]) > *:focus-within,
  :where(ui-toggle-group, [data-ui~="toggle-group"]) > *:has(> input:checked) {
    z-index: 1;
  }

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

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

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

  :where(ui-toggle-group, [data-ui~="toggle-group"]):not([data-toggle-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;
  }

  /* ===========================================================================
  TOGGLE GROUP: VERTICAL — data-toggle-group-orientation="vertical"
  =========================================================================== */

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

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

  :where(
      ui-toggle-group[data-toggle-group-orientation="vertical"],
      [data-ui~="toggle-group"][data-toggle-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

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

Requires: toggle.

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.

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