Toggle

An on/off switch for a single setting that applies immediately — a styled checkbox with an optional label and supporting text, in two sizes and a slim variant.

Examples

Toggle

Off and on. The control is a checkbox with role="switch"; a standalone toggle needs an aria-label, a labelled one gets its name from the label text.

<div style="display:flex;flex-direction:row;flex-wrap:wrap;align-items:flex-start;gap:1.5rem">
<input type="checkbox" role="switch" class="kompan-toggle__input" aria-label="Notifications">
<input type="checkbox" role="switch" class="kompan-toggle__input" aria-label="Notifications" checked>
<label class="kompan-toggle">
  <input type="checkbox" role="switch" class="kompan-toggle__input">
  <span class="kompan-toggle__text">
    <span class="kompan-toggle__label">Remember me</span>
  </span>
</label>
<label class="kompan-toggle">
  <input type="checkbox" role="switch" class="kompan-toggle__input" checked>
  <span class="kompan-toggle__text">
    <span class="kompan-toggle__label">Remember me</span>
  </span>
</label>
</div>

Sizes

sm (default, 36×20 track, 14px text) and md (44×24 track, 16px text). Put the size modifier on the label, or on the input when it stands alone.

<div style="display:flex;flex-direction:row;flex-wrap:wrap;align-items:flex-start;gap:2rem">
<label class="kompan-toggle kompan-toggle_sm">
  <input type="checkbox" role="switch" class="kompan-toggle__input" checked>
  <span class="kompan-toggle__text">
    <span class="kompan-toggle__label">Remember me</span>
    <span class="kompan-toggle__hint">Save my login details for next time.</span>
  </span>
</label>
<label class="kompan-toggle kompan-toggle_md">
  <input type="checkbox" role="switch" class="kompan-toggle__input" checked>
  <span class="kompan-toggle__text">
    <span class="kompan-toggle__label">Remember me</span>
    <span class="kompan-toggle__hint">Save my login details for next time.</span>
  </span>
</label>
</div>

Slim

A lighter track with the knob as tall as the track and a hairline border — for dense settings lists.

<div style="display:flex;flex-direction:row;flex-wrap:wrap;align-items:flex-start;gap:1.5rem">
<input type="checkbox" role="switch" class="kompan-toggle__input kompan-toggle_slim" aria-label="Notifications">
<input type="checkbox" role="switch" class="kompan-toggle__input kompan-toggle_slim" aria-label="Notifications" checked>
<input type="checkbox" role="switch" class="kompan-toggle__input kompan-toggle_slim kompan-toggle_md" aria-label="Notifications">
<input type="checkbox" role="switch" class="kompan-toggle__input kompan-toggle_slim kompan-toggle_md" aria-label="Notifications" checked>
<label class="kompan-toggle kompan-toggle_slim">
  <input type="checkbox" role="switch" class="kompan-toggle__input" checked>
  <span class="kompan-toggle__text">
    <span class="kompan-toggle__label">Remember me</span>
    <span class="kompan-toggle__hint">Save my login details for next time.</span>
  </span>
</label>
</div>

With supporting text

Label plus a one-line explanation. The track centres on the first text line, so the hint can wrap freely.

<div style="display:flex;flex-direction:column;flex-wrap:wrap;align-items:flex-start;gap:1.5rem">
<label class="kompan-toggle">
  <input type="checkbox" role="switch" class="kompan-toggle__input">
  <span class="kompan-toggle__text">
    <span class="kompan-toggle__label">Remember me</span>
    <span class="kompan-toggle__hint">Save my login details for next time.</span>
  </span>
</label>
<label class="kompan-toggle">
  <input type="checkbox" role="switch" class="kompan-toggle__input" checked>
  <span class="kompan-toggle__text">
    <span class="kompan-toggle__label">Remember me</span>
    <span class="kompan-toggle__hint">Save my login details for next time.</span>
  </span>
</label>
<label class="kompan-toggle kompan-toggle_md">
  <input type="checkbox" role="switch" class="kompan-toggle__input" checked>
  <span class="kompan-toggle__text">
    <span class="kompan-toggle__label">Remember me</span>
    <span class="kompan-toggle__hint">Save my login details for next time.</span>
  </span>
</label>
</div>

States

Disabled off / on for both types. Hover darkens the on-track only (Figma has no off-state hover); focus shows the 2px + 4px ring.

<div style="display:flex;flex-direction:row;flex-wrap:wrap;align-items:flex-start;gap:1.5rem">
<label class="kompan-toggle">
  <input type="checkbox" role="switch" class="kompan-toggle__input" disabled>
  <span class="kompan-toggle__text">
    <span class="kompan-toggle__label">Disabled</span>
  </span>
</label>
<label class="kompan-toggle">
  <input type="checkbox" role="switch" class="kompan-toggle__input" disabled checked>
  <span class="kompan-toggle__text">
    <span class="kompan-toggle__label">Disabled</span>
  </span>
</label>
<label class="kompan-toggle kompan-toggle_slim">
  <input type="checkbox" role="switch" class="kompan-toggle__input" disabled>
  <span class="kompan-toggle__text">
    <span class="kompan-toggle__label">Disabled</span>
  </span>
</label>
<label class="kompan-toggle kompan-toggle_slim">
  <input type="checkbox" role="switch" class="kompan-toggle__input" disabled checked>
  <span class="kompan-toggle__text">
    <span class="kompan-toggle__label">Disabled</span>
  </span>
</label>
</div>

Settings list

Toggles stacked in a form. Each setting applies immediately when switched — no submit button.

<form style="display:grid;gap:1.25rem;max-width:22rem">
<label class="kompan-toggle">
  <input type="checkbox" role="switch" class="kompan-toggle__input" checked>
  <span class="kompan-toggle__text">
    <span class="kompan-toggle__label">Email notifications</span>
    <span class="kompan-toggle__hint">Get a summary of activity every morning.</span>
  </span>
</label>
<label class="kompan-toggle">
  <input type="checkbox" role="switch" class="kompan-toggle__input">
  <span class="kompan-toggle__text">
    <span class="kompan-toggle__label">Push notifications</span>
    <span class="kompan-toggle__hint">Alerts on your phone for mentions and replies.</span>
  </span>
</label>
<label class="kompan-toggle">
  <input type="checkbox" role="switch" class="kompan-toggle__input" checked>
  <span class="kompan-toggle__text">
    <span class="kompan-toggle__label">Two-factor authentication</span>
    <span class="kompan-toggle__hint">Ask for a code every time you sign in.</span>
  </span>
</label>
</form>

Accessibility

It is a switch, not a checkbox

Keep role="switch" on the input so screen readers announce "on/off" instead of "checked/unchecked". Use a toggle only for a setting that takes effect immediately; if the choice is submitted with a form, use a checkbox instead.

Every toggle has a name

Inside label.kompan-toggle the label text names the control. A standalone input.kompan-toggle__input must carry aria-label or aria-labelledby. The supporting text is not part of the name — link it with aria-describedby if it matters for the decision.

State is the checked attribute

The on/off look follows :checked, so the state is always in the DOM and exposed to assistive technology. Do not toggle classes to fake it, and do not use the disabled state to show an "on but locked" setting without also explaining why.

Keyboard and focus

Space toggles the switch; the 2px + 4px focus ring appears on :focus-visible. Do not remove it. Clicking the label or supporting text also toggles, which gives a large target — keep the label wrapping the input.

Motion

The knob slides over 150ms; the transition is disabled under prefers-reduced-motion.

Contrast

The label is 60% black on white (about 5.7:1, AA). The supporting text is 50% black (about 4.2:1) — AA only at 14px+ regular, so keep it at the component sizes. The off-state track (10% black) is decorative; the knob shadow gives it shape.

Stylesheet

Shipped as @wah-kompan/kompan-ui/components/toggle.css. Every value is a token reference, so it themes with the rest of the system rather than being restyled.

css
.kompan-toggle,
.kompan-toggle__input {
  /* Size ramp — sm is the Figma default (36×20 track, 16px knob) */
  --kompan-toggle-w: 2.25rem;
  --kompan-toggle-h: 1.25rem;
  --kompan-toggle-pad: var(--spacing-xxs);
  --kompan-toggle-gap: var(--spacing-md);
  --kompan-toggle-text-gap: 0px;
  --kompan-toggle-font-size: var(--type-font-size-sm);
  --kompan-toggle-line-height: var(--type-line-height-sm);

  /* Colour roles */
  --kompan-toggle-track: var(--color-input-bg-secondary);
  --kompan-toggle-track-on: var(--color-input-bg-selected);
  --kompan-toggle-track-on-hover: var(--color-input-bg-selected-hover);
  --kompan-toggle-track-disabled: var(--color-input-bg-disabled);
  --kompan-toggle-knob: var(--color-input-fg-selected);
  --kompan-toggle-knob-border: transparent;
  --kompan-toggle-knob-shadow: 0 1px 2px -1px var(--color-utility-alpha-black-10), 0 1px 3px 0 var(--color-utility-alpha-black-10);
  --kompan-toggle-focus-ring: 0 0 0 2px var(--color-bg-primary), 0 0 0 4px var(--color-button-focus-ring);
}

.kompan-toggle_md {
  --kompan-toggle-w: 2.75rem;
  --kompan-toggle-h: 1.5rem;
  --kompan-toggle-gap: var(--spacing-lg);
  --kompan-toggle-text-gap: var(--spacing-xxs);
  --kompan-toggle-font-size: var(--type-font-size-md);
  --kompan-toggle-line-height: var(--type-line-height-md);
}

/* Slim: no inset, knob as tall as the track, hairline knob border */
.kompan-toggle_slim {
  --kompan-toggle-w: 2rem;
  --kompan-toggle-h: 1rem;
  --kompan-toggle-pad: 0px;
  --kompan-toggle-track-disabled: var(--color-input-bg-secondary);
  --kompan-toggle-knob-border: var(--color-input-border-primary);
  --kompan-toggle-knob-shadow: 0 1px 2px 0 var(--color-utility-alpha-black-5);
}

.kompan-toggle_slim.kompan-toggle_md {
  --kompan-toggle-w: 2.5rem;
  --kompan-toggle-h: 1.25rem;
}

/* Label wrapper */
.kompan-toggle {
  display: inline-flex;
  align-items: flex-start;
  gap: var(--kompan-toggle-gap);
  margin: 0;
  font-family: var(--type-font-family-body);
  font-size: var(--kompan-toggle-font-size);
  line-height: var(--kompan-toggle-line-height);
  cursor: pointer;
}

.kompan-toggle:has(.kompan-toggle__input:disabled) {
  cursor: not-allowed;
}

/* Centre the track on the first text line (Figma's 2px wrapper padding for Slim) */
.kompan-toggle > .kompan-toggle__input {
  margin-top: calc((var(--kompan-toggle-line-height) - var(--kompan-toggle-h)) / 2);
}

/* The control — a checkbox drawn as a track, knob in ::before */
.kompan-toggle__input {
  appearance: none;
  flex: none;
  display: inline-block;
  box-sizing: border-box;
  width: var(--kompan-toggle-w);
  height: var(--kompan-toggle-h);
  margin: 0;
  padding: var(--kompan-toggle-pad);
  border: 0;
  border-radius: var(--radius-full);
  background: var(--kompan-toggle-track);
  vertical-align: middle;
  cursor: pointer;
  transition: background-color 0.15s ease;
}

.kompan-toggle__input::before {
  content: "";
  display: block;
  box-sizing: border-box;
  width: calc(var(--kompan-toggle-h) - 2 * var(--kompan-toggle-pad));
  height: calc(var(--kompan-toggle-h) - 2 * var(--kompan-toggle-pad));
  border-radius: 50%;
  background: var(--kompan-toggle-knob);
  box-shadow: inset 0 0 0 var(--stroke-sm) var(--kompan-toggle-knob-border), var(--kompan-toggle-knob-shadow);
  transform: translateX(0);
  transition: transform 0.15s ease, box-shadow 0.15s ease;
}

.kompan-toggle__input:checked {
  background: var(--kompan-toggle-track-on);
  --kompan-toggle-knob-border: var(--kompan-toggle-track-on);
}

.kompan-toggle__input:checked::before {
  transform: translateX(calc(var(--kompan-toggle-w) - var(--kompan-toggle-h)));
}

.kompan-toggle__input:checked:hover {
  background: var(--kompan-toggle-track-on-hover);
  --kompan-toggle-knob-border: var(--kompan-toggle-track-on-hover);
}

.kompan-toggle__input:focus-visible {
  outline: none;
  box-shadow: var(--kompan-toggle-focus-ring);
}

.kompan-toggle__input:disabled,
.kompan-toggle__input:disabled:checked {
  --kompan-toggle-knob-border: transparent;
  background: var(--kompan-toggle-track-disabled);
  cursor: not-allowed;
}

.kompan-toggle_slim .kompan-toggle__input:disabled,
.kompan-toggle__input.kompan-toggle_slim:disabled {
  --kompan-toggle-knob-border: var(--color-input-border-primary);
}

.kompan-toggle__input:disabled::before {
  opacity: 0.5;
}

@media (prefers-reduced-motion: reduce) {
  .kompan-toggle__input,
  .kompan-toggle__input::before {
    transition: none;
  }
}

/* Text */
.kompan-toggle__text {
  display: flex;
  flex-direction: column;
  gap: var(--kompan-toggle-text-gap);
  min-width: 0;
}

.kompan-toggle__label {
  color: var(--color-text-secondary);
  font-weight: var(--type-font-weight-medium);
}

.kompan-toggle__hint {
  color: var(--color-text-tertiary);
  font-weight: var(--type-font-weight-regular);
}