HTML Formatterhtmlformatteronline.com

Toggle switch

A switch built on a real checkbox, with sizes, labels and a disabled state.

csstoggleswitchcheckboxform
<div class="stack">

  <label class="switch">
    <input type="checkbox" checked>
    <span class="track" aria-hidden="true"><span class="knob"></span></span>
    <span class="text">
      <strong>Email notifications</strong>
      <span>A summary when a deploy finishes.</span>
    </span>
  </label>

  <label class="switch">
    <input type="checkbox">
    <span class="track" aria-hidden="true"><span class="knob"></span></span>
    <span class="text">
      <strong>Weekly digest</strong>
      <span>Every Monday morning, in your timezone.</span>
    </span>
  </label>

  <label class="switch">
    <input type="checkbox" disabled>
    <span class="track" aria-hidden="true"><span class="knob"></span></span>
    <span class="text">
      <strong>SMS alerts</strong>
      <span>Available on the Agency plan.</span>
    </span>
  </label>

  <hr>

  <div class="row">
    <label class="switch sm inline">
      <input type="checkbox" checked>
      <span class="track" aria-hidden="true"><span class="knob"></span></span>
      <span class="text"><strong>Small</strong></span>
    </label>

    <label class="switch inline">
      <input type="checkbox" checked>
      <span class="track" aria-hidden="true"><span class="knob"></span></span>
      <span class="text"><strong>Default</strong></span>
    </label>

    <label class="switch lg inline">
      <input type="checkbox" checked>
      <span class="track" aria-hidden="true"><span class="knob"></span></span>
      <span class="text"><strong>Large</strong></span>
    </label>
  </div>

</div>

How it works

It is a checkbox. The input is visually hidden but present and focusable, so Tab reaches it, Space toggles it, screen readers announce "checkbox, checked", and it submits with the form. None of that has to be built.

Hidden with opacity, not display: none. display: none and visibility: hidden both remove the element from the accessibility tree and from the tab order, which would defeat the entire approach. A 1×1 transparent input stays real.

The whole thing is a label. Wrapping the input, the track and the text in one <label> means clicking anywhere — including the description — toggles the switch. No for attribute and no generated ids needed.

Sizes come from three custom properties. The knob size and its travel distance are both calculated from them, so a new size is one line and nothing has to be measured by hand.

The state is driven by :checked. The visual can never drift out of step with the control, which is the failure mode of switches built from divs and a class.

:has() for the disabled cursor. .switch:has(input:disabled) styles the label based on its child's state — something that previously needed a class on the parent.

Accessibility notes

The track and knob are aria-hidden: they are a picture of the checkbox's state, and announcing them would duplicate what the input already says.

The focus ring is on the track via input:focus-visible + .track, because the real input is one pixel across and its own outline would be invisible.

disabled removes the control from the tab order entirely. If the reason it is unavailable matters — "Available on the Agency plan" — consider aria-disabled="true" instead, which keeps it focusable so the explanation can be read.

Label both what the switch controls and what state means what. "Email notifications" is clear; a bare switch with no text beside it is a guess.

Making it yours

Should it be role="switch"? Both are defensible. A checkbox announces "checked" and a switch announces "on", and the switch role better matches a control that takes effect immediately rather than on submit. Adding role="switch" to the input gives you that — but keep the checkbox underneath either way.

For a switch that saves immediately, give feedback. A silent toggle leaves people wondering whether it took. A brief toast from the alerts template is usually enough.

Do not use a switch for something that only applies when a form is submitted. A checkbox is the honest control there, because a switch implies the change is already live.

Related templates

Check your version

Once you have edited this, the HTML validator will catch any tag you left unclosed, and the formatter will tidy the indentation. Both run in your browser.