Skip to content

Components

Switch

See also: Switch group

All switches should have labels. Notice the use of aria-label on the label element.

html
<!-- Checked -->
<label class="switch" aria-label="Label">
  <input type="checkbox" role="switch" />
</label>

<!-- Unchecked. Demos another common pattern where the input is next to the label with the use of the .sr-only class -->
<div class="switch">
  <label for="switch-unchecked" class="sr-only">Label</label>
  <input id="switch-unchecked" type="checkbox" role="switch" />
</div>

<!-- Checked & disabled -->
<label class="switch" aria-label="Label">
  <input type="checkbox" role="switch" checked disabled />
</label>

<!-- Unchecked & disabled -->
<label class="switch" aria-label="Label">
  <input type="checkbox" role="switch" disabled />
</label>

Visible labels

Add an element with the .text class. Also, don't miss the info on label accessibility.

html
<label class="switch">
  <input type="checkbox" role="switch" />
  <span class="label">Label</span>
</label>

Label position

html
<label class="switch">
  <input type="checkbox" role="switch" />
  <span class="label">Label</span>
</label>

Supporting text

html
<label class="switch">
  <input type="checkbox" role="switch" />
  <span class="label">Default</span>
  <span class="supporting-text">Supporting text</span>
</label>

Validation

  • Add [required] to the <input> element to toggle required styles
  • The .error class toggles the error styles. Make use of the supporting text to give extra feedback on the error.
html
<label class="switch error">
  <input type="checkbox" role="switch" required/>
  <span class="label">Default</span>
</label>

<label class="switch error">
  <input type="checkbox" role="switch" />
  <span class="label">Default</span>
  <span class="supporting-text">Supporting text</span>
</label>

Sizes

Add the .small class on the parent for a smaller Switch variant.

html
<label class="switch small">
  <!--  -->
</label>

<label class="switch">
  <!-- -->
</label>

Field group

Legend
html
<fieldset class="field-group">
  <legend>Legend</legend>
  <div class="fields">
    <!--  -->
  </div>
</fieldset>

Direction

Legend
html
<fieldset class="field-group row">
  <!--  -->
</fieldset>

Supporting text

Can be placed above and below the fields.

LegendSupporting text above fields
Legend
Supporting text below fields
html
<fieldset class="field-group row">
  <legend>Legend</legend>
  <span class="supporting-text">Supporting text</span>
  <div class="fields">
    <!--  -->
  </div>
</fieldset>

<fieldset class="field-group row">
  <legend>Legend</legend>
  <div class="fields">
    <!--  -->
  </div>
  <span class="supporting-text">Supporting text</span>
</fieldset>

Disabled

Attach the disabled attribute to the <fieldset> element.

Legend
html
<fieldset class="field-group row" disabled>
  <!--  -->
</fieldset>

Required

Attach the required attribute to at least one of your <input> elements.

These are required!
html
<fieldset class="field-group row">
  <legend>Legend</legend>
  <div class="fields">
    <label class="switch">
      <input type="checkbox" role="switch" required/>
      <span class="label">Switch 1</span>
    </label>
    <!--  -->
  </div>
</fieldset>

Validation

Attach the .error class to your fieldset.field-group element

LegendSomething went wrong!
html
<fieldset class="field-group row error">
  <!--  -->
</fieldset>

Accessibility

Role & attributes

Role/attributeUsage
role="switch"Required on the input element. Identifies the element that serves as a switch.

Labels

To have an accessible label you can choose between three approaches.

VariantUsage in Switch component
Add a aria-label on the elementDefault behavior.
Provide a label inside the elementUsed when showing visible labels.
Have a visible label that you reference with aria-labelledbyNot used.

Keyboard support

KeyFunction
SpaceWhen switch is focused, changes the switch's state.
Enter(Optional) When switch is focused, changes the switch's state.

Anatomy

  1. Container: label element
  2. Switch: & input type="checkbox" role="switch"
  3. Label (optional): & .text

API

TypeModifiersDefaultDescription
Children.label, .supporting-text-Optional children.
Sizes.small, defaultdefaultThe size of the element.
Label position.stack, defaultdefaultIf applied, the label is stacked under the Switch.
Validation.error-When applied, error styles are shown.

Field group API

TypeModifiersDefaultDescription
Children<legend>, .fields, .supporting-text, checkbox, radio, switch-Supported child elements.
Directioncolumn, .rowcolumnDecides which direction the inputs will be placed.
Disabled[disabled]-When applied, disabled styles are shown.
Validation.error-When applied, error styles are shown.

Browser compatibility

Installation

css
@layer components.base {
  :where(.switch) {
    --_accent-color: var(--primary);
    --_accent-contrast: var(--primary-contrast);

    --_dot-bg-color: light-dark(var(--gray-11), var(--gray-14));
    --_dot-inset: var(--size-1) auto auto var(--size-1);
    --_dot-outline-size: 0;
    --_dot-size: var(--size-3);

    --_track-bg-color: light-dark(var(--gray-3), var(--gray-8));
    --_track-height: var(--size-5);
    --_track-width: var(--size-8);
    --_transition-tf: var(--ease-4);
    --_transition-time: 0.2s;

    align-items: center;
    color: var(--text-color-1);
    display: inline-grid;
    gap: 0 var(--size-2);
    grid-auto-columns: auto;
    grid-auto-flow: column;
    inline-size: fit-content;

    input[type="checkbox"][role="switch"] {
      appearance: none;
      block-size: var(--_track-height);
      cursor: pointer;
      inline-size: var(--_track-width);
      margin: 0;
      position: relative;

      /* Track */
      &:before {
        background-color: var(--_track-bg-color);
        block-size: var(--_track-height);
        border: 1px solid var(--_dot-bg-color);
        border-radius: 100vmax;
        content: "";
        inline-size: var(--_track-width);
        inset: 0;
        position: absolute;
      }

      &:focus-visible {
        outline-offset: 2px;
        outline: 2px solid currentColor;
      }

      /* Dot */
      &:after {
        background-color: var(--_dot-bg-color);
        block-size: var(--_dot-size);
        border-radius: 100vmax;
        border: 1px solid var(--_dot-border-color);
        content: "";
        inline-size: var(--_dot-size);
        inset: var(--_dot-inset);
        outline-offset: -1px;
        outline: var(--_dot-outline-size) solid var(--_dot-bg-color);
        position: absolute;
      }

      /* Checked */
      &:checked {
        &:before {
          background-color: var(--_accent-color);
          border-color: var(--_accent-color);
          transition:
            background-color var(--_transition-time) var(--_transition-tf),
            border-color var(--_transition-time) var(--_transition-tf);
        }

        /* Dot */
        &:after {
          --_dot-bg-color: var(--_accent-contrast);
          --_dot-outline-size: calc(var(--size-1) - 1px);

          inset-inline-start: calc(
            var(--_track-width) - var(--_dot-size) - var(--size-1)
          );
        }
      }

      /* Animation */
      @media (prefers-reduced-motion: no-preference) {
        /* Track */
        &:before {
          transition:
            background-color var(--_transition-time) var(--_transition-tf),
            border-color var(--_transition-time) var(--_transition-tf);
        }

        /* Dot */
        &:after {
          transition: all var(--_transition-time) var(--_transition-tf);
        }

        &:active:after {
          --_dot-outline-size: calc(var(--size-1) + 1px);
        }

        &:checked {
          &:active:after {
            --_dot-outline-size: calc(var(--size-1) + 1px);
          }
        }
      }
    }

    /* Required dot */
    &:has([required]:not(:checked)) {
      .label:after {
        color: var(--red);
        content: "*";
        inset: 0 -0.25ex auto auto;
        position: absolute;
      }
    }

    /* Disabled */
    &:has([disabled]) {
      cursor: not-allowed;
      opacity: 0.64;
      user-select: none;

      input {
        cursor: not-allowed;
      }
    }

    /* Label */
    .label {
      grid-column: 2;
      grid-row: 1;
      min-width: 0;
      padding-inline: 0 1ex;
      position: relative;
      user-select: none;
    }

    /* Supporting text */
    .supporting-text {
      color: var(--text-color-2);
      font-size: var(--font-size-xs);
      grid-column: 2;
      grid-row: 2;
      line-height: 1.5;
      z-index: 1;
    }

    /* Size */
    &.small {
      --_dot-size: 0.75rem;
      --_track-height: var(--size-4);
      --_track-width: 2.5rem;
    }

    /* Stacked layout */
    &.stack {
      justify-items: center;
      grid-auto-columns: unset;

      .label {
        grid-column: 1/-1;
        grid-row: 2;
        margin-block-start: var(--size-1);
        padding-inline: 1ex;
      }

      .supporting-text {
        grid-column: 1/-1;
        grid-row: 3;
      }
    }

    /* Validation */
    &.error {
      input {
        outline: 2px solid var(--color-9);
        border-radius: var(--radius-round);
      }

      .label,
      .supporting-text {
        color: var(--color-9);
      }
    }
  }
}
css
@layer components.has-deps {
  /* Common styling for checkbox, radio and switch groups */
  :where(fieldset.field-group) {
    border: 0;
    border-radius: 0;
    gap: 0;
    padding: 0;
    z-index: 1;

    legend {
      color: var(--text-color-2);
      padding: 0 1ex 0 0;
    }

    /* Disabled */
    &[disabled] {
      cursor: not-allowed;
      opacity: 0.64;
      user-select: none;

      input {
        cursor: not-allowed;
      }
    }

    /* Validation */
    &.error {
      legend,
      .supporting-text {
        color: var(--color-9);
      }
    }

    /* Required */
    &:has([required]) {
      &:not(:has(input:where([type="radio"], [type="checkbox"]):checked)) {
        legend {
          position: relative;

          &:after {
            color: var(--red);
            content: "*";
            inset: 0 -0.25ex auto auto;
            position: absolute;
          }
        }
      }
    }
    :where(.radio, .checkbox, .switch) .label:after {
      display: none;
    }

    /* Supporting text */
    .supporting-text {
      color: var(--text-color-2);
      font-size: var(--font-size-xs);
      line-height: 1.5;
      z-index: 1;
    }

    /* Fields */
    .fields {
      display: flex;
      flex-direction: column;
      gap: var(--size-2);

      * ~ & {
        padding: var(--size-2) 0;
      }
    }

    :last-child {
      padding-block-end: 0;
    }

    /* Directions */
    &.row {
      .fields {
        flex-direction: row;
      }
    }
  }
}