Creating an Accessible Toggle Switch in Tailwind CSS

A row of switches with "on" above each switch and "off" below each switch. Some switches are in the "on"position and others are "off"
Kyndrea Martinez

UX Developer

Kyndrea Martinez

You need to reach as many users for your digital product as possible. Accessible design is how you do that. Book a free consult to learn how we bake accessiblity in from the start to maximize your user base.

In the early versions of Tailwind, there would often need to be additional logic written into components for conditionally toggling long strings of utility classes. Things would get even messier when that logic involved styling descendent elements based on the parent element’s state. With the addition of Tailwind’s aria-* modifiers and group helper classes, conditionally styling components has gotten significantly cleaner and more legible. Let’s take a look at this in practice by creating an accessible toggle switch component in Tailwind.

We’ll start out by styling a simple toggle button in its default “off” state

<button type="button" id="toggle-switch" class="flex w-14 h-8 p-1 rounded-full bg-slate-400 overflow-hidden outline-none focus-visible:ring focus-visible:ring-purple-400">
  <span class="w-6 h-6 rounded-full bg-white"></span>
</button>
<label for="toggle-switch">
  Switch off
</label>

This will create a left-aligned circle within a gray container, indicating its “off” state. It’s also good practice to include accompanying text to clear up any ambiguity on the state of this toggle switch, so we aren’t relying on visual cues only.

The aria-checked Attribute

Next, we’ll need to add styling for the “on” state. One approach is to toggle an .active class on the button to visually distinguish between “on” and “off” states. But this adds more complexity and classes. Instead, we can leverage the state of ARIA attributes to conditionally style our button component. Tailwind provides a few attribute selector utility classes out of the box.

For this component, we will be using the ARIA switch role. This role can only have two possible values: “on” and “off.” When using role="switch", the aria-checked attribute is required. The aria-checked attribute indicates the current “checked” state of checkboxes, radio buttons, and other widgets.

We can add the aria-checked: utility class to change the button background color when aria-checked’s value is true

<button type="button" id="toggle-switch" role="switch" aria-checked="true" class="flex w-14 h-8 p-1 rounded-full bg-slate-400 transition-colors duration-200 ease-in-out overflow-hidden outline-none focus-visible:ring focus-visible:ring-purple-400 aria-checked:bg-blue-400">
  <span class="w-6 h-6 rounded-full bg-white"></span>
</button>
<label for="toggle-switch">
  Switch on
</label>

The group Helper Class

This changes the background color, but we’ll also want to change the positioning of the inner circle to indicate that the toggle is now switched on. In CSS, you can do this by targeting the nested span within the button. You can also achieve this in Tailwind by using an arbitrary variant. This approach would look like: class="[&>span]:aria-checked:translate-x-full"

While this works and does keep all styling inline, there can be gotchas down the road as the project grows: What if the inner element changes and the selector no longer works? What if more markup is added later? We’d need to create more arbitrary variants for each affected element, and that can become cumbersome and hard to keep readable.

Enter Tailwind’s group helper class. These classes are helpful when you need to style elements based on the state of the parent element. Add a group class to the parent, and use group-* modifiers to style the target element.

In our case, we want to conditionally set the circle’s (child) position based on the button’s (parent) aria-checked value. Adding the appropriate group classes in our markup achieves this.

<button type="button" id="toggle-switch" role="switch" aria-checked="false" class="group flex w-14 h-8 p-1 rounded-full bg-slate-400 transition-colors duration-200 ease-in-out overflow-hidden outline-none focus-visible:ring focus-visible:ring-purple-400 aria-checked:bg-blue-400">
  <span class="w-6 h-6 rounded-full bg-white transition-transform duration-200 ease-in-out group-aria-checked:translate-x-full"></span>
</button>
<label for="toggle-switch">
  Switch off
</label>

Final Result

The resulting component will only rely on keeping track of the button’s aria-checked value as it is clicked. All conditional styling will be handled by Tailwind’s aria-checked and group classes.

Conclusion

Using Tailwind’s built-in aria-* modifiers and group helper classes cleans up our inline markup and allows us to conditionally style both parent and child elements seamlessly. This aids in trimming down the amount of additional logic that has to be written into our components, as well as keeping our code concise and legible as the project grows.

Newsletter

Stay in the Know

Get the latest news and insights on Elixir, Phoenix, machine learning, product strategy, and more—delivered straight to your inbox.

Narwin holding a press release sheet while opening the DockYard brand kit box