Toggle

Form element that allows users to switch between two states, such as on and off.

Class name Type
.prs-toggle Component For <input>
.prs-toggle-label Component For <label>
.prs-toggle-warning Modifier Red variant
.prs-toggle-confirmation Modifier Green variant
.prs-toggle-sm Modifier Small size
.prs-toggle_focus State Manually apply visual focus
.prs-toggle_disabled State Manually apply visual disabled
  • Default

    Use the native HTML checkbox because it provides the most accurate semantics for a toggle switch with the simplest markup. It allows users to understand the state of the toggle easily, and it is natively accessible to screen readers.

    <label><input type="checkbox" class="prs-toggle" aria-label="Toggle" /></label>
    <label><input type="checkbox" class="prs-toggle" aria-label="Toggle" checked /></label>
    
  • With label

    An example with a little help from Tailwind CSS.

    <div class="w-96 max-w-xs divide-y space-y-5 [&>:where(:not(:first-child))]:pt-5"> <!-- <- this is just for demo purposes -->
      <label class="prs-toggle-label">
        <span class="prs-label-text">Label at the start</span>
        <input type="checkbox" class="prs-toggle" />
      </label>
      <label class="prs-toggle-label">
        <input type="checkbox" class="prs-toggle" />
        <span class="prs-label-text">Label at the end that breaks to multiple lines and still looks decent. Please try to maintain one line!</span>
      </label>
    </div>
    
  • <label><input type="checkbox" class="prs-toggle prs-toggle-warning" aria-label="Toggle" checked /></label>
    
  • <label><input type="checkbox" class="prs-toggle prs-toggle-confirmation" aria-label="Toggle" checked /></label>
    
  • <label><input type="checkbox" class="prs-toggle prs-toggle-sm" aria-label="Toggle" /></label>
    
  • Icon

    Use icons to represent the toggle states. The first icon is for the "unchecked" state, and the second one is for the "checked" state.

    <label class="prs-toggle">
      <input type="checkbox" aria-label="Toggle" />
      <span><svg xmlns="http://www.w3.org/2000/svg" fill="currentColor" role="img"><use href="/_assets/prs-icons.svg#nav-x" /></svg></span>
      <span><svg xmlns="http://www.w3.org/2000/svg" fill="currentColor" role="img"><use href="/_assets/prs-icons.svg#status-check" /></svg></span>
    </label>
    <label class="prs-toggle prs-toggle-sm">
      <input type="checkbox" aria-label="Toggle" />
      <span><svg xmlns="http://www.w3.org/2000/svg" fill="currentColor" role="img"><use href="/_assets/prs-icons.svg#nav-x" /></svg></span>
      <span><svg xmlns="http://www.w3.org/2000/svg" fill="currentColor" role="img"><use href="/_assets/prs-icons.svg#status-check" /></svg></span>
    </label>
    
  • Indeterminate

    The indeterminate state is useful for checkboxes that represent a mixed state, such as when some but not all items are selected.

    <label>
      <input
        type="checkbox"
        class="prs-toggle"
        aria-label="Toggle"
        x-init="$el.indeterminate = true"
        @click="$el.indeterminate = !$el.checked"
      />
    </label>
    <label>
      <input
        type="checkbox"
        class="prs-toggle prs-toggle-sm"
        aria-label="Toggle"
        x-init="$el.indeterminate = true"
        @click="$el.indeterminate = !$el.checked"
      />
    </label>
    
  • Focus

    Focus styles will apply automatically, but we also provide .prs-toggle_focus if you need to manually apply the focus state.

    <label><input type="checkbox" class="prs-toggle prs-toggle_focus" aria-label="Toggle" /></label>
    <label><input type="checkbox" class="prs-toggle prs-toggle_focus" aria-label="Toggle" checked /></label>
    <label><input type="checkbox" class="prs-toggle prs-toggle-sm prs-toggle_focus" aria-label="Toggle" /></label>
    <label><input type="checkbox" class="prs-toggle prs-toggle-sm prs-toggle_focus" aria-label="Toggle" checked /></label>
    
  • <label><input type="checkbox" class="prs-toggle" aria-label="Toggle" disabled /></label>
    <label><input type="checkbox" class="prs-toggle" aria-label="Toggle" checked disabled /></label>
    <label><input type="checkbox" class="prs-toggle prs-toggle-warning" aria-label="Toggle" checked disabled /></label>
    <label><input type="checkbox" class="prs-toggle prs-toggle-confirmation" aria-label="Toggle" checked disabled /></label>
    

CSS

Full Library

Component Only

.prs-toggle {
  --toggle-size: 1.75rem;
  --toggle-pad: 1px;
  --toggle-border: 1px;
  --toggle-content: '';
  --toggle-color: var(--prs-c-gray-600);
  --toggle-radius-max: calc(var(--prs-radius-badge) + var(--prs-radius-badge) + var(--prs-radius-badge));
  appearance: none;
  padding: var(--toggle-pad);
  border: var(--toggle-border) solid var(--toggle-color);
  width: calc((var(--toggle-size) * 2) - (var(--toggle-border) + var(--toggle-pad)) * 3);
  height: var(--toggle-size);
  background-color: var(--toggle-color);
  color: var(--prs-c-white);
  vertical-align: middle;
  display: inline-grid;
  flex-shrink: 0;
  grid-template-columns: 0fr 1fr 1fr;
  place-content: center;
  position: relative;
  cursor: pointer;
  -webkit-user-select: none;
  user-select: none;
  border-radius: calc(var(--prs-radius-badge) + min(var(--toggle-pad), var(--toggle-radius-max)) + min(var(--toggle-border), var(--toggle-radius-max)));
  box-shadow: 0 1px color-mix(in oklab, currentColor calc(var(--depth) * 10%), #0000) inset;
  transition: background-color 0.3s, color 0.3s, grid-template-columns 0.2s;
  & > * {
    appearance: none;
    padding: 0;
    border: none;
    height: 100%;
    cursor: pointer;
    background-color: transparent;
    color: var(--prs-c-gray-800);
    display: flex;
    grid-column: 2 / span 1;
    grid-row-start: 1;
    align-items: center;
    justify-content: center;
    z-index: 1;
    transition: opacity 0.2s, rotate 0.4s;
    &:focus-visible {
      outline: 0 none;
    }
    &:nth-child(2) {
      rotate: none;
    }
    &:nth-child(3) {
      opacity: 0;
      rotate: -15deg;
    }
    &:is(svg),& > :is(svg) {
      width: 1em;
      height: 1em;
    }
  }
  &:has(:checked) {
    & > :nth-child(2) {
      opacity: 0;
      rotate: 15deg;
    }
    & > :nth-child(3) {
      opacity: 1;
      rotate: none;
    }
  }
  &:before {
    height: 100%;
    aspect-ratio: 1;
    content: var(--toggle-content);
    background-color: currentColor;
    grid-row-start: 1;
    grid-column-start: 2;
    position: relative;
    inset-inline-start: 0;
    border-radius: var(--prs-radius-badge);
    box-shadow: 0 -1px oklch(0% 0 0 / calc(var(--depth) * 0.1)) inset,
    0 8px 0 -4px oklch(100% 0 0 / calc(var(--depth) * 0.1)) inset,
    0 1px color-mix(in oklab, currentColor calc(var(--depth) * 10%), #0000);
    translate: 0;
    transition: background-color 0.1s, translate 0.2s, inset-inline-start 0.2s;
  }
  @media print {
    &:before {
      outline: 0.25rem solid;
      outline-offset: -1rem;
    }
  }
  &:focus-visible,
  &.prs-toggle_focus,
  &:has(:focus-visible),
  &.prs-toggle_focus:has(:focus-visible) {
    outline: 2px solid var(--prs-c-aqua);
    outline-offset: 1px;
  }
  &:checked,
  &[aria-checked="true"],
  &:has(> input:checked) {
    --toggle-color: var(--prs-c-aqua);
    grid-template-columns: 1fr 1fr 0fr;
    &:before {
      background-color: currentColor;
    }
    @starting-style {
      &:before {
        opacity: 0;
      }
    }
  }
  &:indeterminate {
    grid-template-columns: 0.5fr 1fr 0.5fr;
  }
  &:disabled,
  &.prs-toggle_disabled,
  &:has(> input:disabled),
  &.prs-toggle_disabled:has(> input:disabled) {
    cursor: not-allowed;
    opacity: 0.3;
    & > * {
      cursor: not-allowed;
      pointer-events: none;
    }
  }
}
.prs-toggle-warning {
  &:checked,
  &[aria-checked="true"] {
    --toggle-color: var(--prs-c-red);
  }
}
.prs-toggle-confirmation {
  &:checked,
  &[aria-checked="true"] {
    --toggle-color: var(--prs-c-green);
  }
}
.prs-toggle-sm {
  font-size: 0.8125rem;
  &[type='checkbox'],
  &:has([type='checkbox']) {
    --toggle-size: 1.125rem;
    width: calc((var(--toggle-size) * 2) - (var(--toggle-border) + var(--toggle-pad)) * 2);
  }
}

Figma

Coming soon...