Open demo

CSS code

HTML
<div><span class="lbl">ON / OFF in the track</span><div class="row">
<label class="t"><input type="checkbox" role="switch" aria-label="Notifications" checked><span class="tr"><span class="on" aria-hidden="true">ON</span><span class="off" aria-hidden="true">OFF</span></span></label>
<label class="t"><input type="checkbox" role="switch" aria-label="Location access"><span class="tr"><span class="on" aria-hidden="true">ON</span><span class="off" aria-hidden="true">OFF</span></span></label></div></div>
<div><span class="lbl">Label beside the switch</span><div class="row">
<label class="t side"><input type="checkbox" role="switch" checked><span class="tr"></span>Email notifications</label>
<label class="t side"><input type="checkbox" role="switch"><span class="tr"></span>Weekly digest</label></div></div>
<div><span class="lbl">Two-word switch</span><div class="row">
<label class="seg"><input type="checkbox" role="switch" aria-label="Yearly billing"><em></em><b>Monthly</b><b>Yearly</b></label>
<label class="seg"><input type="checkbox" role="switch" aria-label="Dark theme" checked><em></em><b>Light</b><b>Dark</b></label></div></div>
CSS
/* Labelled switches: a visually hidden checkbox followed by the element that draws the state. */
.t input,.seg input{position:absolute;opacity:0;width:0;height:0;margin:0}
.t{position:relative;display:inline-flex;align-items:center;cursor:pointer;gap:12px;font-size:.9rem}
.tr{position:relative;flex-shrink:0;width:78px;height:32px;background:#64748b;border-radius:999px;transition:background .22s}
.tr::before{content:"";position:absolute;top:3px;left:3px;width:26px;height:26px;background:#fff;border-radius:50%;
  box-shadow:0 1px 3px rgba(15,23,42,.3);transition:transform .22s;z-index:2}
.tr span{position:absolute;top:0;height:32px;line-height:32px;font-size:.7rem;font-weight:700;
  letter-spacing:.08em;color:#fff;transition:opacity .18s}
.tr .on{left:12px;opacity:0}.tr .off{right:12px;opacity:1}
.t input:checked+.tr{background:#15803d}
.t input:checked+.tr::before{transform:translateX(46px)}
.t input:checked+.tr .on{opacity:1}
.t input:checked+.tr .off{opacity:0}
.t input:focus-visible+.tr{outline:2px solid #2563eb;outline-offset:3px}
/* label beside the switch */
.side .tr{width:52px;height:30px}
.side .tr::before{width:24px;height:24px}
.side input:checked+.tr{background:#2563eb}
.side input:checked+.tr::before{transform:translateX(22px)}
/* two-word switch */
.seg{display:inline-flex;background:#e2e8f0;border-radius:999px;padding:4px;position:relative;cursor:pointer}
.seg em{position:absolute;top:4px;left:4px;width:calc(50% - 4px);height:calc(100% - 8px);
  background:#fff;border-radius:999px;box-shadow:0 1px 3px rgba(15,23,42,.18);transition:transform .22s}
.seg b{position:relative;z-index:2;padding:7px 20px;font-size:.82rem;font-weight:600;color:#475569;
  transition:color .2s}
.seg input:checked~em{transform:translateX(100%)}
.seg input:not(:checked)~b:first-of-type,.seg input:checked~b:last-of-type{color:#0f172a}
.seg input:focus-visible~em{outline:2px solid #2563eb;outline-offset:2px}
@media (prefers-reduced-motion:reduce){.tr,.tr::before,.tr span,.seg em,.seg b{transition:none}}

Bootstrap 5 code

Requires: Bootstrap 5.3.8 CSS

HTML
<div><span class="lbl">ON / OFF in the track</span><div class="demo-row">
<label class="form-check form-switch sw"><input class="form-check-input" type="checkbox" role="switch" aria-label="Notifications" checked><span class="on" aria-hidden="true">ON</span><span class="off" aria-hidden="true">OFF</span></label>
<label class="form-check form-switch sw"><input class="form-check-input" type="checkbox" role="switch" aria-label="Location access"><span class="on" aria-hidden="true">ON</span><span class="off" aria-hidden="true">OFF</span></label></div></div>
<div><span class="lbl">Label beside the switch</span><div class="demo-row">
<div class="form-check form-switch sw side"><input class="form-check-input" type="checkbox" role="switch" id="sw-email" checked><label class="form-check-label" for="sw-email">Email notifications</label></div>
<div class="form-check form-switch sw side"><input class="form-check-input" type="checkbox" role="switch" id="sw-digest"><label class="form-check-label" for="sw-digest">Weekly digest</label></div></div></div>
<div><span class="lbl">Two-word switch</span><div class="demo-row">
<label class="seg position-relative d-inline-flex rounded-pill p-1 bg-body-secondary"><input class="visually-hidden" type="checkbox" role="switch" aria-label="Yearly billing"><em class="rounded-pill"></em><b>Monthly</b><b>Yearly</b></label>
<label class="seg position-relative d-inline-flex rounded-pill p-1 bg-body-secondary"><input class="visually-hidden" type="checkbox" role="switch" aria-label="Dark theme" checked><em class="rounded-pill"></em><b>Light</b><b>Dark</b></label></div></div>
CSS
/* Bootstrap .form-switch, resized and recoloured; the knob is the --bs-form-switch-bg image. */
.sw{--sw-off:#64748b;--sw-on:#15803d;--sw-focus:#2563eb;
  --sw-knob:url("data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='-16 -16 32 32'><filter id='s' x='-50%25' y='-50%25' width='200%25' height='200%25'><feDropShadow dy='1' stdDeviation='1.5' flood-color='%230f172a' flood-opacity='.3'/></filter><circle r='13' fill='%23fff' filter='url(%23s)'/></svg>");
  position:relative;display:inline-flex;align-items:center;gap:12px;min-height:0;margin:0;padding:0;font-size:.9rem}
.sw .form-check-input{width:78px;height:32px;margin:0;float:none;flex-shrink:0;border:0;border-radius:999px;cursor:pointer;
  background-color:var(--sw-off);transition:background-position .22s,background-color .22s}
.sw .form-check-input,.sw .form-check-input:focus,.sw .form-check-input:checked{--bs-form-switch-bg:var(--sw-knob)}
.sw .form-check-input:checked{background-color:var(--sw-on)}
.sw .form-check-input:focus{box-shadow:none}
.sw .form-check-input:focus-visible{outline:2px solid var(--sw-focus);outline-offset:3px}
.sw .form-check-label{cursor:pointer}
/* ON / OFF words over the track */
.sw .on,.sw .off{position:absolute;top:0;height:32px;line-height:32px;font-size:.7rem;font-weight:700;
  letter-spacing:.08em;color:#fff;pointer-events:none;transition:opacity .18s}
.sw .on{left:12px;opacity:0}.sw .off{right:12px}
.sw .form-check-input:checked~.on{opacity:1}
.sw .form-check-input:checked~.off{opacity:0}
/* label beside the switch */
.sw.side{--sw-on:#2563eb;
  --sw-knob:url("data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='-15 -15 30 30'><filter id='s' x='-50%25' y='-50%25' width='200%25' height='200%25'><feDropShadow dy='1' stdDeviation='1.5' flood-color='%230f172a' flood-opacity='.3'/></filter><circle r='12' fill='%23fff' filter='url(%23s)'/></svg>")}
.sw.side .form-check-input{width:52px;height:30px}
/* two-word switch: a visually hidden checkbox and a sliding pill */
.seg{--bs-secondary-bg-rgb:226,232,240;--seg-muted:#475569;--seg-active:#0f172a;cursor:pointer}
.seg em{position:absolute;top:4px;left:4px;width:calc(50% - 4px);height:calc(100% - 8px);
  background:#fff;box-shadow:0 1px 3px rgba(15,23,42,.18);transition:transform .22s}
.seg b{position:relative;z-index:2;padding:7px 20px;font-size:.82rem;font-weight:600;color:var(--seg-muted);transition:color .2s}
.seg input:checked~em{transform:translateX(100%)}
.seg input:not(:checked)~b:first-of-type,.seg input:checked~b:last-of-type{color:var(--seg-active)}
.seg input:focus-visible~em{outline:2px solid #2563eb;outline-offset:2px}
@media (prefers-reduced-motion:reduce){.sw .form-check-input,.sw .on,.sw .off,.seg em,.seg b{transition:none}}

Tailwind 4 code

Requires: Tailwind CSS 4

HTML
<div><span class="mb-4 block text-[.72rem] tracking-[.12em] text-[#64748b] uppercase">ON / OFF in the track</span><div class="flex flex-wrap items-center gap-[18px]">
<label class="relative inline-block h-8 w-[78px] cursor-pointer"><input type="checkbox" role="switch" aria-label="Notifications" class="peer sr-only" checked><span class="absolute inset-0 rounded-full bg-sw-off transition-[background-color,border-color,color] duration-[220ms] peer-checked:bg-sw-on peer-focus-visible:outline-2 peer-focus-visible:outline-offset-3 peer-focus-visible:outline-sw-accent motion-reduce:transition-none"></span><span aria-hidden="true" class="absolute top-0 left-3 text-[.7rem] leading-8 font-bold tracking-[.08em] text-white opacity-0 transition-opacity duration-[180ms] peer-checked:opacity-100 motion-reduce:transition-none">ON</span><span aria-hidden="true" class="absolute top-0 right-3 text-[.7rem] leading-8 font-bold tracking-[.08em] text-white transition-opacity duration-[180ms] peer-checked:opacity-0 motion-reduce:transition-none">OFF</span><span class="pointer-events-none absolute top-[3px] left-[3px] size-[26px] rounded-full bg-white shadow-[0_1px_3px_rgba(15,23,42,.3)] transition-transform duration-[220ms] peer-checked:translate-x-[46px] motion-reduce:transition-none"></span></label>
<label class="relative inline-block h-8 w-[78px] cursor-pointer"><input type="checkbox" role="switch" aria-label="Location access" class="peer sr-only"><span class="absolute inset-0 rounded-full bg-sw-off transition-[background-color,border-color,color] duration-[220ms] peer-checked:bg-sw-on peer-focus-visible:outline-2 peer-focus-visible:outline-offset-3 peer-focus-visible:outline-sw-accent motion-reduce:transition-none"></span><span aria-hidden="true" class="absolute top-0 left-3 text-[.7rem] leading-8 font-bold tracking-[.08em] text-white opacity-0 transition-opacity duration-[180ms] peer-checked:opacity-100 motion-reduce:transition-none">ON</span><span aria-hidden="true" class="absolute top-0 right-3 text-[.7rem] leading-8 font-bold tracking-[.08em] text-white transition-opacity duration-[180ms] peer-checked:opacity-0 motion-reduce:transition-none">OFF</span><span class="pointer-events-none absolute top-[3px] left-[3px] size-[26px] rounded-full bg-white shadow-[0_1px_3px_rgba(15,23,42,.3)] transition-transform duration-[220ms] peer-checked:translate-x-[46px] motion-reduce:transition-none"></span></label></div></div>
<div><span class="mb-4 block text-[.72rem] tracking-[.12em] text-[#64748b] uppercase">Label beside the switch</span><div class="flex flex-wrap items-center gap-[18px]">
<label class="relative inline-flex cursor-pointer items-center gap-3 text-[.9rem]"><input type="checkbox" role="switch" class="peer sr-only" checked><span class="h-[30px] w-[52px] shrink-0 rounded-full bg-sw-off transition-[background-color,border-color,color] duration-[220ms] peer-checked:bg-sw-accent peer-focus-visible:outline-2 peer-focus-visible:outline-offset-3 peer-focus-visible:outline-sw-accent motion-reduce:transition-none"></span><span class="pointer-events-none absolute top-1/2 left-[3px] size-6 -translate-y-1/2 rounded-full bg-white shadow-[0_1px_3px_rgba(15,23,42,.3)] transition-transform duration-[220ms] peer-checked:translate-x-[22px] motion-reduce:transition-none"></span>Email notifications</label>
<label class="relative inline-flex cursor-pointer items-center gap-3 text-[.9rem]"><input type="checkbox" role="switch" class="peer sr-only"><span class="h-[30px] w-[52px] shrink-0 rounded-full bg-sw-off transition-[background-color,border-color,color] duration-[220ms] peer-checked:bg-sw-accent peer-focus-visible:outline-2 peer-focus-visible:outline-offset-3 peer-focus-visible:outline-sw-accent motion-reduce:transition-none"></span><span class="pointer-events-none absolute top-1/2 left-[3px] size-6 -translate-y-1/2 rounded-full bg-white shadow-[0_1px_3px_rgba(15,23,42,.3)] transition-transform duration-[220ms] peer-checked:translate-x-[22px] motion-reduce:transition-none"></span>Weekly digest</label></div></div>
<div><span class="mb-4 block text-[.72rem] tracking-[.12em] text-[#64748b] uppercase">Two-word switch</span><div class="flex flex-wrap items-center gap-[18px]">
<label class="relative inline-flex cursor-pointer rounded-full bg-seg p-1"><input type="checkbox" role="switch" aria-label="Yearly billing" class="peer sr-only"><em class="absolute top-1 left-1 h-[calc(100%-8px)] w-[calc(50%-4px)] rounded-full bg-white shadow-[0_1px_3px_rgba(15,23,42,.18)] transition-transform duration-[220ms] peer-checked:translate-x-full peer-focus-visible:outline-2 peer-focus-visible:outline-offset-2 peer-focus-visible:outline-sw-accent motion-reduce:transition-none"></em><b class="relative z-[2] px-5 py-[7px] text-[.82rem] font-semibold text-seg-ink transition-colors duration-200 peer-checked:text-seg-muted motion-reduce:transition-none">Monthly</b><b class="relative z-[2] px-5 py-[7px] text-[.82rem] font-semibold text-seg-muted transition-colors duration-200 peer-checked:text-seg-ink motion-reduce:transition-none">Yearly</b></label>
<label class="relative inline-flex cursor-pointer rounded-full bg-seg p-1"><input type="checkbox" role="switch" aria-label="Dark theme" class="peer sr-only" checked><em class="absolute top-1 left-1 h-[calc(100%-8px)] w-[calc(50%-4px)] rounded-full bg-white shadow-[0_1px_3px_rgba(15,23,42,.18)] transition-transform duration-[220ms] peer-checked:translate-x-full peer-focus-visible:outline-2 peer-focus-visible:outline-offset-2 peer-focus-visible:outline-sw-accent motion-reduce:transition-none"></em><b class="relative z-[2] px-5 py-[7px] text-[.82rem] font-semibold text-seg-ink transition-colors duration-200 peer-checked:text-seg-muted motion-reduce:transition-none">Light</b><b class="relative z-[2] px-5 py-[7px] text-[.82rem] font-semibold text-seg-muted transition-colors duration-200 peer-checked:text-seg-ink motion-reduce:transition-none">Dark</b></label></div></div>
CSS
@theme {
  --color-sw-off: #64748b;
  --color-sw-on: #15803d;
  --color-sw-accent: #2563eb;
  --color-seg: #e2e8f0;
  --color-seg-muted: #475569;
  --color-seg-ink: #0f172a;
}

About this set

Switches that say what state they are in, for places where a coloured track alone is not clear enough. The set has three variants. ON and OFF in the track prints the current state inside a 78 pixel track, fading the words as the knob passes. Label beside the switch pairs a standard switch with a text label such as Email notifications, and the whole label is the click target. The two-word switch shows both choices, for example Monthly and Yearly or Light and Dark, with a white pill that slides under the active word. Use the ON and OFF track in admin panels and forms, the side label in settings lists, and the two-word switch for a binary choice where both options deserve a name. Every variant is one checkbox, visually hidden, followed by spans that CSS shows, hides or moves with the :checked selector. No JavaScript is involved and the state submits with a form like any checkbox.

What’s included

  • ON and OFF text inside the track, faded in and out
  • Side label that makes the whole row clickable
  • Two-word switch with a sliding pill for binary choices
  • Darker green and slate tracks so the white ON and OFF text reaches 4.5:1
  • Transitions removed under prefers-reduced-motion

Accessibility

The side-label switches take their name from their visible text; the others carry an aria-label, and the ON and OFF words are aria-hidden so the state is announced once rather than read out as text. All controls use role="switch", are reachable with Tab and toggle with Space. Focus shows a 2px blue outline on the track or on the sliding pill, and motion is disabled under prefers-reduced-motion.

Customise it

Change the track colours in .tr and .t input:checked+.tr (plain CSS), the --sw-on and --sw-off variables on .sw (Bootstrap) or the --color-sw-* theme colours (Tailwind). The two-word switch uses --seg-muted and --seg-active in Bootstrap and seg- colours in Tailwind.