What Is a Tailwind Toggle and Does Tailwind CSS Include One?
Tailwind CSS includes nothing like that. Every toggle on the web is assembled from utilities such as peer-checked, rounded-full and translate-x-5, and the finished control switches a single setting between on and off.
Because Tailwind is a utility-first framework without prebuilt widgets, the toggle is a pattern you copy, not a class you call.
Tailwind Plus (formerly Tailwind UI) has toggles made by the Tailwind CSS team, but it closed to new customers on September 9, 2026, when Tailwind Labs joined Shopify.
You’ll run into two builds. One is the checkbox build, a hidden input plus a styled sibling, with no JavaScript at all. The other is a button with role="switch", styled from aria-checked, where script flips the state.
On screen they look identical. What differs is where the on/off state lives.
How to Build a Toggle Switch Without JavaScript
Two elements do the work, a hidden checkbox and a styled sibling. The sibling picks up the checkbox state through the peer-checked variant, so no script runs.
- Wrap the control in a
label so a click anywhere on it reaches the input. - Add the checkbox with
sr-only peer. - Put a
div after the input as the track, with width, height, rounded-full and the off color. - Draw the thumb with
after: classes on the track: absolute position, size, rounded-full and a white fill. - Add
peer-checked:bg-* to the track and peer-checked:after:translate-x-* to move the thumb. - Finish with a text span for the visible label.
Here’s a version that works.
<label class="inline-flex items-center cursor-pointer">
<input type="checkbox" class="peer sr-only">
<div class="relative h-6 w-11 rounded-full bg-gray-200 transition-colors duration-300
peer-checked:bg-blue-600 peer-focus-visible:ring-2 peer-focus-visible:ring-blue-300
after:absolute after:left-0.5 after:top-0.5 after:h-5 after:w-5 after:rounded-full
after:bg-white after:transition-transform after:duration-300
peer-checked:after:translate-x-5"></div>
<span class="ms-3 text-sm text-gray-900">Notifications</span>
</label>
Flowbite’s toggle docs use the same structure, down to the thumb drawn with an after: pseudo-element.
According to the Tailwind CSS documentation, the sr-only utility sets position: absolute and overflow: hidden, along with a 1px width and height and a negative 1px margin. The input leaves the layout but stays available to screen readers, as the Tailgrids toggle docs point out.
Some tutorials hide the input with w-0 h-0 opacity-0 instead (KindaCode does). It works. I’d still go with sr-only, since it’s the documented utility for the job.
The input has to come before the track in the DOM. That’s the one rule you can’t bend, because the Tailwind CSS documentation limits the peer marker to previous siblings.
The same checkbox technique carries over to a toggle switch written in plain CSS, where the :checked selector replaces peer-checked.
Paste the markup into Tailwind Play to check the thumb travel before it goes into a project.
How to Style Toggle States, Sizes and Animation
Variants handle state. Width and height set the size, and the thumb’s slide comes from translate-x-* plus a duration.
Checked, disabled and focus classes
The Tailwind CSS documentation says the peer pattern works with every pseudo-class variant, including peer-focus and peer-disabled.
- Checked styling goes on the track with
peer-checked:bg-blue-600. - For focus,
peer-focus-visible:ring-2 lets keyboard users see where they are. - Disabled means the
disabled attribute on the input and a muted label color (Flowbite uses its text-fg-disabled token). - Flowbite also documents disabled and checked together as a separate look of its own.
Material Tailwind takes another route for disabled: pointer-events-none on the parent div, with opacity-50 on the input and the thumb. The whole control dims and stops responding at once.
Size classes and thumb travel distance
Thumb travel equals the track width, minus the thumb width, minus the inset on both sides. Get that sum wrong and the thumb stops short or overshoots the track.
| Source | Track | Thumb | Thumb travel |
|---|
| Material Tailwind (HTML switch) | w-11 h-5 | w-5 h-5 | translate-x-6 |
| Bordered track pattern | w-11 h-6, border-2 | w-5 h-5 | translate-x-5 |
| DockYard (ARIA button) | w-14 h-8, p-1 | w-6 h-6 | translate-x-full |
Every row obeys the rule, which is derived here from the published class values. Material Tailwind has no inset, so 11 minus 5 gives 6.
The bordered track loses one unit to its 2px borders (11 – 5 – 1 = 5), since the default spacing unit is 4px. DockYard loses two units to p-1 padding (14 – 6 – 2 = 6), which equals the thumb width and explains translate-x-full.
If you just want ready-made sizes, Flowbite shows a default toggle (w-9 h-5) and a large one (w-11 h-6). Tailgrids ships sm and md, with sm as the default.
Transition utilities and duration
duration-300 sets transition-duration: 300ms, per the Tailwind CSS documentation.
The Headless UI switch example uses plain transition, which defaults to 150ms in Tailwind, and Material Tailwind uses duration-300. Some tutorials push it to duration-700, which feels slow on a control people expect to flip at once.
Put transition-colors on the track and transition-transform on the thumb. transition-all (Flowbite’s choice) works too, but it animates every property that changes.
The sliding thumb is a small micro-interaction, so motion-sensitive users should be able to switch it off. Tailwind’s variants cover prefers-reduced-motion, which means a motion-reduce:transition-none class removes the slide.
How Do Dark Mode and Color Variants Work on a Toggle?
Add a dark: variant to every class that sets a color. By default dark: follows the prefers-color-scheme media query, so the toggle adapts to the operating system with no configuration.
That covers the off track, the on track, the thumb, the border, the label text and the focus ring.
Flowbite’s color examples show the pattern with dark:bg-gray-700 on the track and a dark:peer-checked: twin for each checked color.
Color variants come from swapping the color inside the peer-checked and peer-focus classes. That is how Flowbite builds its orange and other color examples.
Using a toggle to switch the site theme
Manual theme switching needs the dark variant keyed to a class or attribute instead of the media query. In Tailwind v3 that is darkMode: 'selector', which replaced the class strategy in v3.4.1, according to the Tailwind CSS documentation.
In Tailwind v4 the same change lives in the stylesheet: @custom-variant dark (&:where(.dark, .dark *));.
The toggle then adds or removes the dark class on the html element and saves the choice to localStorage. Initialise the checkbox from that stored value, or it shows the wrong position after a reload.
An inline script in the document head applies the stored theme on load, which fixes the flash of the wrong theme (Tailwind CSS documentation).
A two-position toggle can’t express “follow the system”, though. Tailwind’s documentation describes three-way theme toggles for that case, built on window.matchMedia().
How Do You Make a Tailwind Toggle Accessible?
Give the control a switch role, a state, an accessible name, Space-key support and a visible focus style. A native checkbox covers most of that. A button needs role="switch" and an aria-checked value set by script.
The W3C ARIA Authoring Practices Guide switch pattern asks for the following.
- Role switch, or a native checkbox, which uses its
checked attribute instead of aria-checked - A name from visible text,
aria-labelledby or aria-label - Space changes the state. Enter is optional
aria-checked is true when on and false when off
ARIA attributes matter most on the button build, where nothing native reports the state. On the checkbox build the browser does that work, which is the main argument for it on static pages.
The label shouldn’t change when the state does, and the W3C pattern says so too.
“Notifications” stays “Notifications”. The switch announces on or off, so a screen reader says “Notifications switch on” instead of “Notifications checkbox checked”.
Sources disagree on the role. Some older toggle markup puts role="checkbox" with aria-checked on a span, while the W3C documents three switch implementations: a div, a button and a checkbox input.
The W3C also calls switch, checkbox and toggle button functionally interchangeable in many cases. Pick the role whose announcement matches the setting.
Hiding the input with sr-only removes its visible outline, so draw the ring on the track with peer-focus-visible.
WCAG 2.1 success criterion 1.4.11 requires user interface components and their states to reach a 3:1 contrast ratio against adjacent colors, with disabled controls exempt. The Headless UI documentation example uses bg-gray-200 for the off track, a pale gray close to a white page, so check the color contrast of the off state against that 3:1 threshold.
Checkbox, ARIA Button or Headless UI: Which Approach Fits?
Mostly this comes down to who owns the state. With the checkbox build the DOM does. The button build keeps it in the aria-checked attribute, and with a Headless UI Switch your app holds it.
| Approach | JavaScript | State lives in | Form value |
|---|
| Checkbox build | None | The input’s checked state | Native, sent only when checked |
| Button build | Yes, flips aria-checked | The aria-checked attribute | None by default |
| Headless UI Switch | React or Vue | Component state | Via the name prop |
The checkbox build is a custom-styled checkbox, which makes it the cheapest option and the right default when nothing outside the DOM needs the state. Nothing to install, nothing to hydrate.
Headless UI is the heavier choice, and not always the better one. Its documentation says the Switch offers the same semantics and keyboard navigation as a native checkbox, so on a plain HTML page it adds a dependency without adding behavior.
What it does add is control: a controlled value, state exposed to your classes, and a renderless structure that composes with animation libraries such as Framer Motion and React Spring.
Reach for the button build when script is already on the page and every visual state should key off aria-checked. It’s the only build where the markup does nothing until you write the state flip.
How to Use a Tailwind Toggle in React, Vue and Alpine.js
In each framework you bind the toggle to a boolean and let that value drive the same peer classes. The markup stays as it is. Only the state wiring changes.
React
A controlled checkbox needs two props, checked and onChange, according to the React documentation.
const [on, setOn] = useState(false)
<input type="checkbox" className="peer sr-only" checked={on} onChange={(e) => setOn(e.target.checked)} />
If Tailwind is not set up in the project yet, the steps for using Tailwind CSS in React come first.
For the button build, the Tailwind UI JavaScript integration guide pairs an onClick that flips state with an aria-checked value read from that same state.
One gotcha. An input cannot switch between controlled and uncontrolled during its lifetime, so choose checked or defaultChecked up front (React documentation).
Vue
v-model on a single checkbox gives you a boolean. Under the hood, checkboxes use the checked property and the change event. And v-model ignores a static checked attribute, so the starting value belongs in data.
All of that comes from the Vue form input bindings guide. The peer classes need no changes.
Alpine.js
Alpine needs a script tag and nothing else.
Put x-data="{ on: false }" on the label and x-model="on" on the checkbox. The Alpine documentation describes x-model as two-way bound: it both sets and gets, so the input and its data stay in sync.
On the checkbox build, x-model goes on the hidden input. The button build pairs @click="on = !on" with :aria-checked="on".
Headless UI Switch
Headless UI handles ARIA attributes, keyboard interaction and focus, the work that makes accessible UI components costly to build by hand, according to Tailwind Labs’ announcement of its React and Vue support.
The Switch renders a button and exposes its state through data attributes. Controlled use takes checked and onChange, while uncontrolled use takes defaultChecked (Headless UI documentation).
Version 1 gives you Switch.Group, Switch.Label and Switch.Description. State is styled with ui-checked: modifiers from the @headlessui/tailwindcss plugin, and it’s exposed as data-headlessui-state="checked".
Version 2 for React swaps those parts out for Field, Label and Description. The ui-checked: modifiers become data-[checked]: (Tailwind v4 also accepts the shorter data-checked:), and the Switch exposes a data-checked attribute.
The v2.0.0 changelog adds Field, Label and Description, and v2.0.4 marks SwitchGroup as deprecated, so any tutorial built on Switch.Group is written for v1. The Vue docs still sit under the v1 path.
import { Field, Label, Switch } from '@headlessui/react'
import { useState } from 'react'
function Example() {
const [enabled, setEnabled] = useState(false)
return (
<Field>
<Label>Enable notifications</Label>
<Switch
checked={enabled}
onChange={setEnabled}
className="group inline-flex h-6 w-11 items-center rounded-full bg-gray-200 transition data-[checked]:bg-blue-600"
>
<span className="size-4 translate-x-1 rounded-full bg-white transition group-data-[checked]:translate-x-6" />
</Switch>
</Field>
)
}
The thumb starts one unit in and stops one unit from the edge: translate-x-1 to translate-x-6 on a w-11 track.
Prebuilt Toggle Components Worth Using
Tailwind Plus, Flowbite, daisyUI, Preline UI, Material Tailwind and shadcn/ui all ship a Tailwind toggle, and what you copy from each one differs. Some hand you markup, daisyUI hands you a class, and shadcn/ui puts a component file in your repo.
Tailwind Plus
Tailwind Plus was sold as a one-time license for individuals and teams. On September 9, 2026, Tailwind Labs announced it was joining Shopify and closed sign-ups for new customers; existing customers keep their access (Tailwind CSS blog).
Its toggle examples came in React, Vue and plain HTML, and the React and Vue versions run on Headless UI. The Catalyst UI Kit adds a styled Switch that extends the Headless UI Switch, with a color prop that defaults to dark/zinc.
If you don’t already own it, it’s no longer an option, and for a single toggle the checkbox build costs nothing anyway.
Flowbite
Flowbite gives you copy-paste HTML built on a peer checkbox, with no component runtime. It suits prototypes and server-rendered pages well. The catch is that framework state wiring is yours to add.
If the checkbox build is what you want and you’d rather not type it, Flowbite is the shortcut.
daisyUI
One class replaces the whole build.
Put toggle on an input type="checkbox" and you’re done. Colors run from toggle-primary and toggle-success to toggle-error and five more, and sizes go from toggle-xs to toggle-xl.
The daisyUI docs describe the toggle as a checkbox styled to look like a switch, and they override its colors with ordinary Tailwind classes such as checked:bg-orange-400. The trade-off is that the look follows the daisyUI theme, so matching an unrelated brand means overriding it class by class.
Preline UI
Preline UI is an open-source set of prebuilt components based on Tailwind CSS, and its toggle sits on the switch docs page. By default it depends on the Tailwind CSS Forms plugin, which the docs tell you to install.
Watch the naming, though. Preline’s collapse docs use “toggle” for showing and hiding content, so the on/off control is the one filed under switch.
Material Tailwind
Here the checkbox itself is the track (appearance-none) and a label acts as the thumb. Examples cover colors, labels, a disabled state and a ripple effect.
Pick it when you want the appearance-none approach, which drops the separate track div.
shadcn/ui
shadcn/ui adds a Switch with pnpm dlx shadcn@latest add switch, and it comes with disabled, aria-invalid and size options. The docs now offer Base UI, React Aria and Radix UI versions of the component (shadcn/ui documentation).
The component file lands in your codebase, so the track and thumb classes are yours to edit.
Radix and Base UI name the handler onCheckedChange, where Headless UI uses onChange. Supabase’s design system documents its Switch, built on Radix UI, wired to a form field that way.
It’s the natural choice when the project already runs shadcn/ui. Otherwise it brings in a primitive package (Radix UI or Base UI) for a single control.
When a CSS-Only Toggle Stops Working
A CSS-only toggle breaks wherever state has to live outside the DOM or a layout rule gets in the way.
| Situation | What breaks | Fix |
|---|
| Track cannot follow the input | peer-checked never fires | has-[:checked] on the wrapping label |
| React controlled input | checked without onChange | Add the handler, or use defaultChecked |
| Vue v-model | Static checked attribute ignored | Set the starting value in data |
| Plain form post | Unchecked box sends nothing | Read a missing field as off |
| Screen reader | Announced as a checkbox | role=”switch” on the input |
For the layout case, Tailwind’s has-* variant styles an element from the state of its descendants, so has-[:checked]:bg-blue-600 on a wrapping label works when the track cannot follow the input. Check :has() support for your target browsers first.
The Vue row catches people who copy a snippet with a static checked attribute. The Vue guide says v-model ignores initial checked values and treats the bound data as the source of truth.
The form row follows from browser behavior, since an unchecked box sends no value at all. The Vue guide makes the same point when it recommends radio inputs for a guaranteed yes or no.
The announcement row costs nothing to fix. PrimeReact’s InputSwitch puts the switch role on a hidden native checkbox, and the same attribute works on your own input.
One mistake to avoid. Hiding the input with hidden (display: none) removes it from the tab order, and the toggle stops responding to the keyboard.
The button build has its own limit. Until July 2025 the plain HTML examples in Tailwind Plus left aria-checked and the rest of the interactive behavior to your script. Its Elements library has made every HTML block interactive since, but in your own markup the state flip is still your job.
Inside a form, the toggle needs the same label and error handling as any other field, which is the ground accessible forms cover.
On a static page or prototype where script reads the checkbox directly, CSS-only still wins. None of the failures above apply there.
Tailwind Toggle FAQ
Is a toggle the same as a switch?
In everyday use, yes, but the W3C pattern separates them. A switch holds only on or off, while a toggle button can also be partially pressed, according to the ARIA Authoring Practices Guide.
Can a toggle show and hide content instead of switching a setting?
In some libraries, yes. Metronic’s KTUI Toggle component shows and hides elements, which is a different control from an on/off switch.
For a setting, build the switch. For content, flip the hidden class from script.
How do I add icons inside the thumb?
Use a real span as the thumb instead of an after: pseudo-element, and put two icons inside it. Mark the label with group, then show each icon with group-has-[:checked]:block or hide it with hidden.
How do I move the label to the left of the toggle?
Put the text span before the input inside the label, and swap ms-3 for me-3.
The peer rule only needs the track to follow the input, so the text can sit anywhere else.
Do toggle classes change between Tailwind v3 and v4?
The toggle classes stay the same. peer, peer-checked and sr-only read alike in the v3 and current docs. Only the dark mode setup changes, with a config option in v3 and a CSS @custom-variant in v4.
Is Tailwind Plus free?
No. Tailwind Plus was a paid, one-time license, and it has been closed to new customers since September 9, 2026.
The checkbox build needs only Tailwind CSS, so a single toggle never required the purchase.
Testing a Tailwind Toggle Before It Ships
The order I’d test in is keyboard first, then screen reader, then form payload.
- Tab to the control and press Space.
- Listen for the label and an on or off state.
- Submit with the toggle off and read the request.
The keyboard pass comes first because it needs no extra tool. The payload check comes last because it only matters when a server reads the value.
The W3C’s own switch example carries a notice that support gaps remain in some browser and assistive technology combinations, especially on mobile and touch devices. A desktop pass alone leaves touch untested, and the manual screen reader session is the cost no linter replaces.
Repeat the sequence after each library upgrade, and keep a web accessibility checklist beside it for the rest of the page.