Canvas Design System
Main Site Tokens

Toggle

A switch for a setting with two states that takes effect the moment it's flipped.

When to Use

Use when: The setting is on or off and applies immediately — notification preferences, profile visibility, feature opt-ins. It replaces the hand-rolled toggle-* and launchpad-toggle-* markup with one accessible checkbox-based switch.
Don't use when: The change only lands on submit — that's a checkbox, and a switch that silently doesn't save is a trust bug. For more than two options use Radio Card Group or Select.

Variants

Bare switch

Without a label the switch has no accessible name, so only use this form when a visible label sits beside it in your own markup — a settings row where the left column names the setting.

<?= ws_toggle('demo_off') ?>
<?= ws_toggle('demo_on', ['checked' => true]) ?>

With a label

The preferred form. The label becomes the switch's accessible name and its click target, so the hit area covers the text as well as the switch.

Subscribe to digest
<?= ws_toggle('digest', ['label' => 'Subscribe to digest', 'checked' => true]) ?>

With a description

Add a description when the consequence isn't obvious from the label. It's wired as aria-describedby, so it's announced after the label rather than merged into it.

Subscribe to digest Weekly facilitation tips in your inbox
Public profile Let other facilitators find your workshops
<?= ws_toggle('marketing_emails', [
    'checked'     => true,
    'label'       => 'Subscribe to digest',
    'description' => 'Weekly facilitation tips in your inbox',
]) ?>

States

Locked off
Locked on
StateBehavior
OffDefault. Track is --gray-200, knob sits left.
OnTrack fills with the brand colour and the knob slides right over --transition-base. Driven by the native :checked state, not a class.
HoverNo distinct hover treatment on the track — the label's pointer cursor is the affordance.
FocusThe real <input type="checkbox"> is visually hidden but still focusable, so the browser's focus ring lands on the switch. Don't replace the input with a styled <div>; this is what makes it keyboard-operable.
DisabledSets the native disabled attribute and adds .ws-toggle-container--disabled, which dims the whole row. Works in both on and off positions, as above.
<?= ws_toggle('locked_off', ['disabled' => true, 'label' => 'Locked off']) ?>

Real-World Usage

A notification settings panel: each toggle names the setting, describes the consequence, and persists on change rather than waiting for a save button — which is the contract that justifies a switch over a checkbox.

Weekly digest A Saturday roundup of new exercises and articles
Session reminders An email the morning of any workshop you have scheduled
Account and security emails Always on &mdash; these cover password changes and billing
<?= ws_toggle('notify_digest', [
    'checked'     => $prefs['digest'],
    'label'       => 'Weekly digest',
    'description' => 'A Saturday roundup of new exercises and articles',
    'attrs'       => ['data-pref' => 'digest'],
]) ?>

<script>
// Persist on change — a switch must not need a save button.
document.querySelectorAll('[data-pref]').forEach(el =>
    el.addEventListener('change', () => savePref(el.dataset.pref, el.checked)));
</script>

Options

OptionTypeDefaultPurpose
$namestring''Positional. The input's name — effectively required, since it identifies the setting on submit.
checkedboolfalseInitial state. Drive it from stored preference, never hardcode.
labelstringnullTrailing label. Also becomes the accessible name via aria-labelledby.
descriptionstringnullSub-text under the label, wired as aria-describedby
disabledboolfalseNative disabled state plus the dimmed container class
idstring'ws-toggle-' . uniqid()Input ID. Auto-generated so labels always bind correctly — only set it if you need a stable JS handle.
classstring''Additional CSS classes on the wrapper
attrsarray[]Extra HTML attributes on the checkbox input — where data-* hooks and event bindings go

Accessibility

This is among the best-wired components in the library — it builds on a native checkbox rather than simulating one.

ConcernBehavior
SemanticsA real <input type="checkbox">, visually hidden and styled through its adjacent slider. Checked state, focusability, and form submission all come free.
KeyboardNative: Tab to reach, Space to flip. Disabled toggles are skipped.
LabellingThe label element gets an ID and is referenced by aria-labelledby; the description gets its own ID via aria-describedby. Both are announced, in that order.
Unlabelled togglesYour responsibility. Calling ws_toggle() without a label produces a switch with no accessible name. Either pass a label or point attrs => ['aria-label' => …] at the visible text beside it.
Announced as a checkboxBecause the control is a checkbox, screen readers say "checkbox, checked" rather than "switch, on". Functionally equivalent and reliably supported — adding role="switch" would change the wording but risks losing native behaviour, so it isn't done.
Immediate effectFlipping a switch changes something right away with no confirmation step. If the change is destructive or slow, either confirm it or surface the result — silence reads as failure.

Tokens

TokenUsed for
--gray-200Track in the off position
--color-primary / --phase-learn / --planner-primaryTrack fill when on, per context
--color-ink / --color-ink-muted / --color-ink-faintLabel, description, and disabled text
--radius-fullTrack and knob shape
--shadow-smKnob elevation
--space-3Gap between the switch and the label block
--text-ui / --text-captionLabel and description type scale
--font-body / --font-mediumFace and label weight
--transition-baseKnob slide and track colour change

CSS Classes

ClassPurpose
.ws-toggle-containerRow wrapper holding the switch and the label block
.ws-toggle-container--disabledDimmed disabled row
.ws-toggleThe switch itself — the label element wrapping input and slider
.ws-toggle__inputThe visually hidden checkbox. Focusable; do not display: none it.
.ws-toggle__sliderTrack and knob, driven by the input's :checked state
.ws-toggle__label-wrapLabel plus description column
.ws-toggle__labelLabel text
.ws-toggle__descriptionSub-text

Files

FilePurpose
includes/components/helpers.phpws_toggle() helper function
includes/components/toggle.phpTemplate — ID generation and ARIA wiring
includes/components/components.cssStyles (.ws-toggle rules)