Generated code uses role="switch", aria-checked, aria-label, visible focus ring, and keyboard operability (Space to toggle, Tab to navigate).
<label class="toggle-wrapper">
<input
type="checkbox"
class="toggle-input"
role="switch"
aria-label="Enable notifications"
>
<span class="toggle-track">
<span class="toggle-thumb"></span>
</span>
<span class="toggle-label">Enable notifications</span>
</label>
<style>
.toggle-wrapper {
display: inline-flex;
align-items: center;
gap: 10px;
cursor: pointer;
user-select: none;
}
.toggle-input {
position: absolute;
opacity: 0;
width: 0;
height: 0;
margin: 0;
}
.toggle-track {
position: relative;
display: inline-block;
width: 44px;
height: 24px;
background: #d1d5db;
border-radius: 12px;
border: none;
transition: background 200ms ease;
flex-shrink: 0;
box-sizing: border-box;
}
.toggle-input:checked + .toggle-track {
background: #3b82f6;
}
.toggle-thumb {
position: absolute;
top: 2px;
left: 2px;
width: 20px;
height: 20px;
background: #ffffff;
border-radius: 10px;
box-shadow: 0 1px 3px rgba(0,0,0,0.25),0 1px 2px rgba(0,0,0,0.15);
transition: transform 200ms ease;
pointer-events: none;
}
.toggle-input:checked + .toggle-track .toggle-thumb {
transform: translateX(20px);
}
.toggle-input:focus-visible + .toggle-track {
outline: 2px solid #3b82f6;
outline-offset: 2px;
}
.toggle-input:disabled + .toggle-track {
opacity: 0.45;
cursor: not-allowed;
}
.toggle-wrapper:has(.toggle-input:disabled) {
cursor: not-allowed;
}
.toggle-label {
font-size: 14px;
font-weight: 500;
color: inherit;
line-height: 1.4;
}
</style>You Might Also Like
CSS Toggle Switch Generator — Accessible Switch UI with Live Preview
About this tool
Design Toggle Switches That Look Good and Work for Everyone
Toggle switches are everywhere in modern UIs — dark mode controls, notification settings, feature flags, privacy preferences. But a toggle that looks good in Figma often ships broken: it can't be reached by keyboard, screen readers announce it as a generic "checkbox" with no context, there's no visible focus ring, and the disabled state is just a dimmed button with no cursor change. Users who navigate without a mouse or use assistive technology are left behind.
This generator closes that gap. Design your toggle visually, and the exported code is accessible from the start — no extra effort required. Every output includes `role="switch"` so screen readers announce "on/off switch" instead of "checkbox", `aria-checked` that updates dynamically with the toggle state, an `aria-label` for context, a `:focus-visible` outline that appears only on keyboard navigation (not on clicks), and a `:disabled` state that disables both interaction and pointer cursor on the full label.
Six state previews update live as you customise: the interactive toggle you can click, default off, default on, focused (showing the keyboard ring), disabled off, and disabled on. This makes it easy to catch contrast issues in the focused or disabled state before you ship — the two states most often forgotten during design review.
Eight presets cover the most common design patterns: modern blue, minimal black, green, purple, amber, outlined, rounded, and square. Each preset is a starting point — adjust the track colors, thumb, focus ring, shape, border, shadow, transition duration, and easing individually. The preview updates instantly with every change.
Five code outputs are generated in parallel: HTML + CSS using a hidden checkbox approach that needs no JavaScript, a React controlled component with state, a Vue 3 Single-File Component using v-model, a Svelte SFC with bind:checked and createEventDispatcher, and a Tailwind CSS version using the peer modifier. All five include the full ARIA attributes and the correct CSS for all six interaction states.
Track icons and thumb icons are built into the CSS — no extra HTML required. Enable the ✓/✕ track icons option to add check and cross marks that fade in and out via CSS ::before/::after. Choose a thumb icon: Power ⏻ always visible inside the thumb, or Day/Night ☀/🌙 that swaps between sun and moon as the toggle changes state.
WCAG contrast check runs live in the preview header. The badge shows the Non-text Contrast ratio (WCAG 1.4.11) between your thumb and track-on color — green AA ✓ when it passes the 3:1 minimum for UI components, red Fail ✗ when it doesn't. The preview background switcher (◼ dark / ◻ light / custom hex) lets you test how the toggle looks against different page backgrounds before exporting.
For building complete form UIs, pair this tool with our CSS Button Generator for submit buttons, and use Flexbox Builder or CSS Grid Builder to lay out the settings panel around your toggles.
Features
- 6 live state previews — interactive, default off, default on, focused, disabled off, disabled on
- 8 presets — Blue, Green, Purple, Amber, Minimal, Outline, Rounded, Square
- Full color control — track off/on, thumb, focus ring, optional border — each with hex input and color picker
- 3 track shapes — Pill (fully rounded), Rounded (6px), Square (2px)
- Track icons — enable ✓/✕ check and cross marks inside the track with CSS ::before/::after, no extra HTML
- Thumb icons — choose None, Power ⏻, or Day/Night ☀/🌙 — sun and moon swap on toggle via CSS transitions
- Preview background switcher — test your toggle on dark, light, or any custom hex background before exporting
- WCAG contrast badge — live Non-text Contrast ratio (WCAG 1.4.11) between thumb and track-on color with AA pass/fail
- Transition control — duration slider (50–600ms) and 6 easing options including spring cubic-bezier
- Label control — text, position (left / right / none), and font size
- HTML + CSS export — hidden checkbox pattern, zero JavaScript needed
- React export — controlled component with useState, onChange prop, full ARIA
- Vue 3 export — SFC with v-model, defineProps, scoped styles
- Svelte export — bind:checked, createEventDispatcher, scoped styles in one SFC
- Tailwind export — peer modifier pattern, arbitrary values for exact pixel control
- Accessible by default — role="switch", aria-checked, aria-label, :focus-visible ring, :disabled state, :has() cursor fix
- No install, no account — runs entirely in the browser
How to Use
- 1Pick a presetClick any of the 8 preset buttons to load a starting configuration — Blue, Green, Purple, Amber, Minimal, Outline, Rounded, or Square. The preview updates immediately across all 6 states.
- 2Set the sizeChoose Small (32×18px), Medium (44×24px), or Large (56×30px) from the Size control. The thumb size scales proportionally and the translate distance recalculates automatically.
- 3Choose the track shapePill gives a fully rounded iOS-style toggle. Rounded uses 6px corners for a softer rectangular look. Square uses 2px corners for a strict UI or admin panel aesthetic.
- 4Customise colorsUse the color pickers to set the track color for the off state, the track color for the on state, the thumb color, and the focus ring color. The preview shows all states simultaneously so you can check contrast in every configuration.
- 5Adjust style detailsToggle the thumb shadow on or off. Add a track border using the Border slider (0–3px) and set its color — useful for outlined or transparent-track designs. Each change updates the preview instantly.
- 6Add iconsEnable Track icons to show a ✓ checkmark on the left and ✕ cross on the right of the track — they fade in and out with the toggle. Pick a Thumb icon: Power ⏻ always visible, or Day/Night ☀/🌙 that swaps on toggle. All implemented via CSS ::before/::after — no extra HTML.
- 7Check contrast and backgroundThe WCAG badge next to the preview header shows the live contrast ratio between your thumb color and track-on color. 3:1 is the WCAG 1.4.11 AA minimum for UI components. Use the dark ◼, light ◻, or custom color buttons to test your toggle against different page backgrounds before exporting.
- 8Tune the transitionDrag the Duration slider (50–600ms) to control animation speed. Select an easing from the dropdown — ease-out for natural deceleration, ease-in-out for a balanced feel, or the spring cubic-bezier for a bouncy overshoot effect.
- 9Configure the labelType your label text — e.g. "Enable notifications" or "Dark mode". Set the position to Left, Right, or None (for icon-only or visually unlabelled toggles where aria-label provides the accessible name). Adjust the font size slider.
- 10Export the codeClick the HTML+CSS, React, Vue 3, or Tailwind tab to switch code outputs. All four are generated simultaneously. Click Copy to copy the current tab's code to your clipboard.
Common Use Cases
:focus shows an outline whenever an element receives focus — including on mouse clicks, which produces an unexpected and often unwanted ring around a toggle every time a user clicks it. :focus-visible is smarter: browsers apply it only when the focus was triggered by keyboard navigation or a non-pointing input device. This means keyboard users always see a clear focus ring (meeting WCAG 2.4.7), while mouse users don't see a ring on click — matching the expected visual behaviour of native operating system controls.
All generated code in this tool uses :focus-visible on the hidden checkbox input. The visible focus ring is drawn on the track element using the adjacent sibling combinator: .toggle-input:focus-visible + .toggle-track. This keeps the focus indicator visually on the toggle track while the actual keyboard focus remains on the underlying input, which is the correct accessible pattern.
Frequently Asked Questions
The most reliable pattern is a visually hidden checkbox (<input type="checkbox">) styled with CSS, paired with role="switch" and aria-label. The checkbox handles keyboard interaction natively — Space bar toggles it, Tab focuses it — so no JavaScript event listeners are needed for basic functionality. The generated code uses this approach: the real checkbox is opacity:0 width:0 height:0 so it is invisible but still in the tab order and operable by keyboard and screen readers.
A toggle switch needs role="switch" so screen readers announce it as a switch rather than a checkbox, aria-checked to reflect the current on/off state, and aria-label or an associated <label> element so the purpose is announced. The generated code includes all three. For React and Vue, aria-checked is bound to the component state so it updates dynamically with each toggle.
Because the underlying element is an <input type="checkbox">, keyboard support is automatic: Tab moves focus to the toggle, Space bar toggles it on or off, and Enter also works in most browsers. No JavaScript is required. The generated CSS uses :focus-visible to show a clear focus ring only when navigating by keyboard, not on mouse clicks — following the WCAG 2.1 Focus Visible guideline (Success Criterion 2.4.7).
The Tailwind output uses the peer modifier correctly. The hidden checkbox gets the peer class. The track div and thumb div are placed after the checkbox in the same parent container — making them siblings — so peer-checked:bg-[color] on the track and peer-checked:translate-x-[n] on the thumb both respond to the checkbox state. This requires no JavaScript. The pattern works with Tailwind v3 and v4. Note: arbitrary values like translate-x-[20px] require that Tailwind scans your source files for these class names — avoid string interpolation when possible.
The React output generates a controlled component that uses useState to track the checked state. The <input> has checked={checked} and onChange that calls setChecked and fires an optional onChange prop callback. aria-checked is bound to the same state so it stays in sync. To use it: import the Toggle component, render <Toggle label="Dark mode" onChange={(isOn) => setDarkMode(isOn)} />. For an uncontrolled version, replace checked/onChange with defaultChecked and drop the useState.
The cursor only applies to the element it is set on — not its parent. If you add cursor: not-allowed to the track or thumb but the outer <label> still has cursor: pointer, the label text area shows a pointer. The fix is .toggle-wrapper:has(.toggle-input:disabled) { cursor: not-allowed } which uses the CSS :has() relational selector to style the outer wrapper when the inner checkbox is disabled. :has() is supported in Chrome 105+, Safari 15.4+, Firefox 121+. For older browsers, toggle a disabled class on the wrapper in JavaScript instead.
CSS transitions run on the compositor thread, meaning they are not blocked by JavaScript execution. The toggle thumb slide uses transition on transform (GPU-accelerated), and the track color change uses transition on background-color. This gives smooth 60fps animations even under main-thread load. For feel: 150ms ease for a snappy switch, 300ms ease-in-out for a softer feel, or cubic-bezier(0.34,1.56,0.64,1) for a spring bounce overshoot. The Duration and Easing controls in the generator let you tune and preview these live.
Enable the Track icons option in the generator — the exported CSS adds .toggle-track::before with content "✓" on the left side (opacity 0 when off, 1 when checked) and .toggle-track::after with content "✕" on the right side (opacity 1 when off, 0 when checked). Both use transition: opacity so they fade smoothly. No HTML changes are needed — it is pure CSS using the adjacent sibling combinator. The icon size scales automatically with the track height.
The Svelte export generates a single-file component with bind:checked for two-way binding, createEventDispatcher to fire a change event the parent can listen to with on:change, and scoped <style> so the CSS does not leak. Props are exported with export let: label, labelPos, checked, and disabled. Use it as: <Toggle bind:checked={darkMode} on:change={e => console.log(e.detail)} />. The component is fully accessible — role="switch", aria-checked, aria-label, :focus-visible ring, and disabled state are all included in the generated CSS.
Under WCAG 2.1 Success Criterion 1.4.11 Non-text Contrast, UI components need a minimum 3:1 contrast ratio against adjacent colors. For a toggle switch, this means the thumb color against the track-on background must be at least 3:1. This is lower than the 4.5:1 required for normal text. The generator shows a live contrast badge — green "AA ✓" when ratio ≥ 3:1, red "Fail ✗" when below. If you see Fail, try a white or dark thumb against a saturated track color, or use the color pickers to adjust until the badge turns green.