Skip to content

Toggle

.bp-toggle is a styled checkbox switch. It wraps a native <input type="checkbox" role="switch"> inside a <label>, so clicking anywhere on the component — track, thumb, or label text — toggles the state. All visual state is driven by CSS using the :checked and :disabled pseudo-classes plus the + sibling combinator. No JavaScript required.

Default

<label class="bp-toggle">
<input class="bp-toggle__input" type="checkbox" role="switch" />
<span class="bp-toggle__track" aria-hidden="true">
<span class="bp-toggle__thumb"></span>
</span>
<span class="bp-toggle__label">Enable notifications</span>
</label>

Checked by default

<label class="bp-toggle">
<input class="bp-toggle__input" type="checkbox" role="switch" checked />
<span class="bp-toggle__track" aria-hidden="true">
<span class="bp-toggle__thumb"></span>
</span>
<span class="bp-toggle__label">Dark mode</span>
</label>

Sizes

<label class="bp-toggle bp-toggle--sm">
<input class="bp-toggle__input" type="checkbox" role="switch" />
<span class="bp-toggle__track" aria-hidden="true">
<span class="bp-toggle__thumb"></span>
</span>
<span class="bp-toggle__label">Small</span>
</label>
<label class="bp-toggle">
<input class="bp-toggle__input" type="checkbox" role="switch" />
<span class="bp-toggle__track" aria-hidden="true">
<span class="bp-toggle__thumb"></span>
</span>
<span class="bp-toggle__label">Medium (default)</span>
</label>
<label class="bp-toggle bp-toggle--lg">
<input class="bp-toggle__input" type="checkbox" role="switch" />
<span class="bp-toggle__track" aria-hidden="true">
<span class="bp-toggle__thumb"></span>
</span>
<span class="bp-toggle__label">Large</span>
</label>

Disabled

<label class="bp-toggle">
<input class="bp-toggle__input" type="checkbox" role="switch" disabled />
<span class="bp-toggle__track" aria-hidden="true">
<span class="bp-toggle__thumb"></span>
</span>
<span class="bp-toggle__label">Disabled (off)</span>
</label>
<label class="bp-toggle">
<input class="bp-toggle__input" type="checkbox" role="switch" checked disabled />
<span class="bp-toggle__track" aria-hidden="true">
<span class="bp-toggle__thumb"></span>
</span>
<span class="bp-toggle__label">Disabled (on)</span>
</label>

No visible label (aria-label on input)

<label class="bp-toggle">
<input
class="bp-toggle__input"
type="checkbox"
role="switch"
aria-label="Enable notifications"
/>
<span class="bp-toggle__track" aria-hidden="true">
<span class="bp-toggle__thumb"></span>
</span>
</label>
VariableDefaultDescription
--toggle-track-bgvar(--bp-color-border)Track background when unchecked
--toggle-track-bg-checkedvar(--bp-primary)Track background when checked
--toggle-track-radiusvar(--bp-radius-full)Track border radius (pill by default)
--toggle-track-width2.75remTrack width (md default)
--toggle-track-height1.5remTrack height (md default)
--toggle-thumb-bg#fffThumb fill color
--toggle-thumb-size1.125remThumb diameter (md default)
--toggle-thumb-shadowvar(--bp-shadow-sm)Thumb drop shadow
--toggle-durationvar(--bp-duration-fast)Transition duration for track color and thumb
--toggle-label-colorvar(--bp-color-text)Label text color
--toggle-label-gapvar(--bp-space-3)Gap between track and label
--toggle-disabled-opacity0.45Opacity of the full component when disabled

Brand colors via public API

<style>
.demo-toggle--brand {
--toggle-track-bg-checked: #7c3aed;
--toggle-thumb-bg: #f5f3ff;
}
.demo-toggle--danger {
--toggle-track-bg-checked: var(--bp-color-error);
}
</style>
<label class="bp-toggle demo-toggle--brand">
<input class="bp-toggle__input" type="checkbox" role="switch" checked />
<span class="bp-toggle__track" aria-hidden="true">
<span class="bp-toggle__thumb"></span>
</span>
<span class="bp-toggle__label">Custom brand color</span>
</label>
<label class="bp-toggle demo-toggle--danger">
<input class="bp-toggle__input" type="checkbox" role="switch" checked />
<span class="bp-toggle__track" aria-hidden="true">
<span class="bp-toggle__thumb"></span>
</span>
<span class="bp-toggle__label">Danger toggle</span>
</label>
  • role="switch" on the <input> communicates the on/off semantics to screen readers. Assistive technology announces the state as “on” or “off” instead of “checked” or “unchecked”.
  • The <label> wrapper means clicking or tapping anywhere — track, thumb, or label text — activates the control. No additional click handlers are needed.
  • When no visible label text is shown, add aria-label directly to the <input> so screen readers can identify the control.
  • The __track and __thumb elements carry aria-hidden="true" because they are purely decorative — the native <input> already exposes the semantic state.
  • Keyboard: Space or Enter toggles the switch. Focus lands on the <input>, not on the track.
  • The focus ring appears on the visible .bp-toggle__track element via the .bp-toggle__input:focus-visible + .bp-toggle__track selector, giving a clear visual indicator without styling the hidden input.
  • Do not rely on color alone: the thumb position (left = off, right = on) provides a second, color-independent signal.
  • WCAG 2.1 AA: ensure the checked track color (--toggle-track-bg-checked) has at least 3:1 contrast against the page background under WCAG 1.4.11 (non-text contrast). The default --bp-primary is chosen to meet this threshold — verify if overriding it.
APIAvailabilityUsed forWithout itPolyfill
:has() Widely available Baseline 2023 cursor: not-allowed on root when input is :disabledCursor stays pointer; the track still loses opacity via the + combinator ruleNone needed — visual degradation only
prefers-reduced-motion Widely available Baseline 2020 Disables thumb slide and track fade transitionsTransitions still play (no functional impact)None needed
  • --_track-bg, --_track-bg-checked, --_track-radius, --_track-width, --_track-height, --_thumb-bg, --_thumb-size, --_thumb-shadow, --_duration, --_label-color, --_label-gap, --_disabled-opacity — component-private resolved values, do not set directly.
  • The input is visually hidden with opacity: 0; position: absolute; width: 0; height: 0 rather than display: none or visibility: hidden — it must remain in the tab order and be interactable.
  • Thumb translation uses calc(var(--_track-width) - var(--_thumb-size) - (var(--_track-height) - var(--_thumb-size))) which simplifies to track-width - track-height. The inner term (track-height - thumb-size) / 2 is the symmetric padding on each side; subtracting it twice gives the travel distance.
  • Size variants (--sm, --lg) override only the three sizing tokens — all proportions self-adjust via the same calc.
  • The transition: none rule inside @media (prefers-reduced-motion: reduce) targets both .bp-toggle__track and .bp-toggle__thumb to suppress both the color fade and the slide animation.