Skip to content

Components

Toggle button group

Groups related buttons by wrapping them with <yourElement class="toggle-button-group" role="group">.

  • Button groups should consist of 2-5 buttons.
  • Don't allow them to wrap onto a new line.
  • If an icon is used without label text make sure the button communicates clearly what it does.

More helpful guidelines.

Button group or Toggle button group?

If you just need to group a bunch of "dumb" (uncontrolled) buttons - use Button group.

If your buttons depend on state (controlled) - use Toggle button group.

html
<div role="group" class="toggle-button-group">
  <button class="selected">
    <svg></svg>
    Selected
  </button>

  <button>Enabled</button>

  <button disabled>Disabled</button>
</div>

Text + icon

Try playing with this example. Notice how the .selected icon replaces the pre-existing one.

Only show two things at once: icon + text or icon + icon.

html
<div role="group" class="toggle-button-group">
  <button class="selected">
    <svg><!-- Selected icon (checkmark) --></svg>
    Label
  </button>
</div>

Icon-only

Don't forget to use aria-label on the <button> element whenever you don't have another way to label your button.

html
<div role="group" class="toggle-button-group">
  <button class="selected" aria-label="Walking">
    <svg><!-- Selected icon (checkmark) --></svg>
    <svg><!-- Main icon --></svg>
  </button>

  <button aria-label="Cycling">
    <svg><!--Main icon --></svg>
  </button>
</div>

Sizes

Choose between three sizes: default, .small and .x-small.

html
<div role="group" class="toggle-button-group">
  <!--  -->
</div>

<div role="group" class="toggle-button-group small">
  <!--  -->
</div>

<div role="group" class="toggle-button-group x-small">
  <!--  -->
</div>

Full width

html
<div role="group" class="toggle-button-group fullwidth">
  <!--  -->
</div>

Vertical orientation

You probably don't need that.

Vertical button groups are largely a legacy design pattern that can be better handled through:

  • Responsive design
  • Selects/Dropdown menus
  • More concise label writing
  • Other UI patterns that better match what you're actually trying to solve

Horizontal groups or alternative patterns altogether usually provide better UX.

Anatomy

  1. Container: <element role="group" class="toggle-button-group">
  2. Buttons: 2-5 <button> elements
  3. Button content: .selected checkmark, text label or icon

API

TypeModifiersDefaultDescription
Button children& > button > svg/label, & > button.selected > svg + svg/label-Child content the buttons support.
Button size.dynamic-If enabled all button widths are made to fit their respective content.
Sizesdefault, .small, .x-small,defaultThe size to use.

Browser compatibility

Installation

css
@layer components.base {
  :where([role="group"].toggle-button-group) {
    --_border-radius: var(--radius-round);
    --_button-padding-inline: clamp(var(--size-2), 3cqi, var(--size-4));
    --_max-width: auto;
    --_icon-size: var(--size-4);

    background-color: var(--surface-default);
    border: 1px solid var(--border-color);
    border-radius: var(--_border-radius);
    display: flex;
    grid-auto-columns: 1fr;
    grid-auto-flow: column;
    max-inline-size: var(--_max-width);
    min-inline-size: max-content;
    overflow: clip;

    /* Size */
    &.small {
      button {
        min-block-size: 2.1875rem; /* 35px */
      }
    }

    &.x-small {
      button {
        min-block-size: var(--size-6); /* 30px */
      }
    }

    &.fullwidth {
      inline-size: 100%;
    }

    /* Button */
    button {
      --_bg-color: transparent;

      align-items: center;
      background: var(--_bg-color) var(--ripple, none);
      border-radius: 0;
      border-inline: 1px solid var(--border-color);
      border-inline-start-width: 0;
      color: var(--text-color-1);
      display: inline-flex;
      flex: auto;
      gap: 1ex;
      justify-content: center;
      min-block-size: 2.5rem; /* 40px */
      min-inline-size: 5ex;
      outline-offset: calc(-1 * var(--size-2));
      padding: 0 var(--_button-padding-inline);
      position: relative;
      user-select: none;

      @media (prefers-reduced-motion: no-preference) {
        transition:
          background-color 0.2s var(--ease-out-3),
          box-shadow 0.2s var(--ease-out-3),
          border-color 0.2s var(--ease-out-3),
          color 0.2s var(--ease-out-3),
          outline-offset 0.05s var(--ease-1);
      }

      /* Element states */
      &:hover {
        --_bg-color: light-dark(oklch(0% 0 0 / 0.04), oklch(100% 0 0 / 0.08));
      }

      &:focus-visible {
        outline-offset: -6px;
      }

      /* Disabled */
      &[disabled] {
        border-color: color-mix(in oklch, var(--border-color) 50%, transparent);
        cursor: not-allowed;
        color: color-mix(in oklch, var(--text-color-1) 30%, transparent);
      }

      &[disabled] + &:not([disabled]):not(:last-of-type) {
        border-inline-end-width: 1px;
      }

      /* Assign border radius for outline */
      &:first-of-type {
        border-bottom-left-radius: var(--_border-radius);
        border-top-left-radius: var(--_border-radius);
      }
      &:last-of-type {
        border-bottom-right-radius: var(--_border-radius);
        border-top-right-radius: var(--_border-radius);
      }

      &:last-of-type {
        border-inline-end-width: 0;
      }

      /* Ripple effect */
      background-position: center;

      &:where(:not([disabled])) {
        &:where(:not(:active):hover) {
          --ripple: radial-gradient(circle, transparent 1%, var(--_bg-color) 1%)
            center/15000%;

          transition: background var(--button-ripple-duration);
        }

        &:where(:hover:active) {
          background-size: var(--button-ripple-size);
          transition: background 0s;
        }
      }

      /* Icons */
      svg {
        inline-size: var(--_icon-size);
        inset-inline-start: calc(var(--_button-padding-inline));

        & + svg {
          max-block-size: var(--size-5);
          max-inline-size: var(--size-7);
        }
      }

      /* Selected */
      &.selected {
        --_bg-color: color-mix(
          in oklch,
          var(--primary) 12%,
          var(--surface-default)
        );

        /* Checkmark */
        svg:first-of-type {
          margin-block-end: -3px;
        }
      }
    }
  }
}