Component Library

← Back to gallery

Toggle switch

Category: Forms · Tags: switch, toggle, setting, on-off, preference

Email notifications

How to use it

Use for a setting that takes effect as soon as it is flipped, such as turning email notifications on or off. Label it with what it controls, not with an instruction, and show the current state in words as well as colour. For several switches on one page, give each label its own id and point that switch's aria-labelledby at it; the script binds each switch once, however many copies of it the page holds. If the change only applies after a Save button, use a checkbox instead.

Accessibility

A native <button> with role="switch" and aria-checked, so it is reached with Tab, flipped with Enter or Space, and announced as a switch that is on or off. Its name comes from the visible label through aria-labelledby. The state is also written as On or Off inside the control, so it never depends on colour alone. White text on the off (#475569) and on (#15803d) backgrounds is 7.6:1 and 5.0:1.

Source (copy-ready)

<div class="dcl-switch">
  <span class="dcl-switch__label" id="dcl-switch-label">Email notifications</span>
  <button type="button" role="switch" aria-checked="false" aria-labelledby="dcl-switch-label" class="dcl-switch__control">
    <span class="dcl-switch__thumb" aria-hidden="true"></span>
    <span class="dcl-switch__state">Off</span>
  </button>
</div>
<style>
.dcl-switch { display: flex; align-items: center; gap: 0.75rem; font: inherit; color: #0f172a; background-color: #ffffff; }
.dcl-switch__label { color: #0f172a; background-color: #ffffff; }
.dcl-switch__control { display: inline-flex; align-items: center; gap: 0.5rem; min-width: 4.5rem; padding: 0.25rem 0.75rem 0.25rem 0.25rem; border: none; border-radius: 999px; font: inherit; font-weight: 600; cursor: pointer; color: #ffffff; background-color: #475569; }
.dcl-switch__control:hover { color: #ffffff; background-color: #334155; }
.dcl-switch__control:focus-visible { outline: 2px solid #1d4ed8; outline-offset: 2px; }
.dcl-switch__control[aria-checked="true"] { flex-direction: row-reverse; padding: 0.25rem 0.25rem 0.25rem 0.75rem; color: #ffffff; background-color: #15803d; }
.dcl-switch__thumb { width: 1.25rem; height: 1.25rem; border-radius: 50%; background-color: #ffffff; }
</style>
<script>
(function () {
  document.querySelectorAll(".dcl-switch__control").forEach(function (control) {
    // Bound once, however many switches -- and copies of this script -- a page has.
    if (control.dataset.dclBound) {
      return;
    }
    control.dataset.dclBound = "true";
    control.addEventListener("click", function () {
      var on = control.getAttribute("aria-checked") !== "true";
      control.setAttribute("aria-checked", on ? "true" : "false");
      control.querySelector(".dcl-switch__state").textContent = on ? "On" : "Off";
    });
  });
})();
</script>

Was this useful? Send feedback.