Beautiful Radio Buttons with Headless UI and Tailwind CSS

Rajkumar Gaur
SliceOfDev
4 min readJan 21, 2023

--

You don’t have to sacrifice accessibility for a custom style!

Photo by Jackson Sophat on Unsplash

Introduction

Overriding the design of input or form elements is generally a complex process.

It is often required to completely hide the default input and overlay it with other stylable elements like div or label .

Not to mention handling the accessibility and different states.

Headless UI provides a more elegant way of implementing custom input elements.

With Headless UI you only need to worry about styling the custom component with CSS classes and it will take care of the rest. Let’s get started!

Below is what we are trying to build.

Installation

Headless UI requires @headlessui/react and @headlessui/tailwindcss to work. The second dependency is optional, we will see how and why it may be helpful later.

We will also be using Tailwind CSS for styling the components. Headless UI provides a Tailwind CSS plugin that makes it easier to style components based on the state. This is optional, plain CSS can also be used.

The above packages are the only setup we need. We can start using the Headless UI components in our code!

Project Structure

We are using a simple React application created using create-react-app . The only extra file needed here will be tailwind.config.js that can be set up from the Tailwind CSS docs.

// project structure

public/
index.html
src/
App.js
index.js
package.json
tailwind.config.js (if using tailsind css)

Creating the Radio Buttons

We will import the RadioGroup component from the HeadlessUI package and use it inside our form component.

Note that the usage of the name prop is only useful when using the radio group inside a form and the values need to be used on submit.

The value and onChange props can be used on RadioGroup to use it as a controlled component.

RadioGroup.Option is used to specify the individual radio buttons inside the radio group and RadioGroup.Label is used to specify labels for the radio group and individual radio buttons.

Any of these components can be styled using the className prop.

import { RadioGroup } from "@headlessui/react";

export default function App() {
const [plan, setPlan] = useState("startup");

// tailwind css classes
const groupLabelClassNames = "text-gray-700 text-xl font-bold block mb-2";
const optionLabelClassNames =
"px-4 border-2 border-solid border-blue-500 rounded-full mx-1";
const ringClassNames =
" ring-2 ring-white ring-opacity-60 ring-offset-2 ring-offset-sky-300";

return (
<form aria-label="Radio Form">
<RadioGroup
value={plan}
onChange={setPlan}
className="block p-5"
name="plan"
>
<RadioGroup.Label className={groupLabelClassNames}>
Plan
</RadioGroup.Label>
{plans.map((plan) => {
return (
<RadioGroup.Option
key={plan.name}
value={plan.name}
className="inline-block focus:outline-none"
>
<RadioGroup.Label
className={optionLabelClassNames}
>
<span aria-hidden="true">⚬ </span>
{plan.label}
</RadioGroup.Label>
</RadioGroup.Option>
);
})}
</RadioGroup>
</form>
);
}

Managing State

There are three ways to manage the state. When I say manage state I mean knowing the status of checked, unchecked, active, etc states of the radio buttons.

Using Tailwind CSS plugin

The @headlessui/tailwindcss plugin needs to be installed in package.json and then add this plugin in the tailwind.config.js file.

// tailwind.config.js
module.exports = {
// ...
plugins: [
require('@headlessui/tailwindcss')
],
}

The state can now be referenced directly in the Tailwind CSS class names. For example, we can use the code below to update the UI when a radio button is checked using the ui-checked: selector.

const optionLabelClassNames =
"text-blue-500 ui-checked:bg-blue-500 ui-checked:text-white";

Using Data Attributes

Headless UI spits out HTML with the data-headlessui-state attribute after rendering. This attribute can be used to style using plain CSS.

// index.css
[data-headlessui-state*="checked"] {
background: blue;
color: white;
}

Using Render Props

Headless UI provides the use of render props for accessing the UI state inside the JSX. Below is our updated code that uses render props.

{plans.map((plan) => {
return (
<RadioGroup.Option
key={plan.name}
value={plan.name}
className="inline-block focus:outline-none"
>
{({ active, checked }) => (
<RadioGroup.Label
className={`${optionLabelClassNames} ${
checked ? " bg-blue-500 text-white" : " text-blue-500"
} ${active ? ringClassNames : ""}`}
>
{checked && <span aria-hidden="true">⚬ </span>}
{plan.label}
</RadioGroup.Label>
)}
</RadioGroup.Option>
);
})}

Accessibility

Headless UI takes care of the best practices for accessibility for non-screen users.

The HTML after rendering contains the respective roles, labels, and descriptions.

For example, the div rendered for the RadioGroup will have the role of radiogroup and the aria-labelledby attribute.

The RadioGroup.Option will have the role of radio and aria-labelledby and aria-describedby if provided. This also has the aria-checked attribute for the currently checked radio button.

Headless UI also takes care of the keyboard users and provides standard radio button navigation using arrow keys and tabs.

Conclusion

Headless UI is a great and easy way to build custom input components with an amazing plugin for Tailwind CSS. The only thing you need to worry about is the CSS if you use Headless UI.

That’s all folks, thanks for reading!

--

--