I am working in a React and Tailwind app. Last week, I worked on implementing the styling of the UI for a view that included a list of items, each of which included a checkbox to change its status.
It was a pretty interesting challenge because to adapt to the design specs, I needed to completely change the appearance of the checkbox. However, I wanted to do this without affecting its functionality, that means, keeping it fully accessible and interactable.
This was the design specification, showing an unmarked and a marked item:
The approach I chose to take is inspired by this great blog post and consists on keeping the checkbox in the screen while hiding it visually and placing an SVG on top of it. All of these are wrapped in the corresponding label element. The checkbox is still interactable but not visible, while the SVG can be styled in any way the design specs require.
Let's see the process in detail. As we will be talking about styling in this blog post, I removed all the functionality related code that was unrelevant from code examples. If you want to see the complete resulting code, you can check it out in the GitHub repo.
The starting point was a <li>
element that included the the checkbox, the item name and a delete button:
<li>
<label>
{`Mark ${name} as purchased`}
<input type="checkbox" onChange={() => {}} id={itemId} />
</label>
<span>{name}</span>
<button>Delete</button>
</li>
And the rendered view:
We will now focus on the checkbox part. As I said before, I used the input label to wrap the checkbox input and an SVG element. I also wrapped the text label with a <span>
element.
I hide the checkbox by adding the opacity-0
Tailwind class and hide the text label visually without hiding it from screen readers, leveraging the sr-only
Tailwind class for this purpose.
The SVG element is the visual replacement for the checkbox, so I applied some classes to style it following my design specs.
<label className="flex items-center justify-center">
<input type="checkbox" id={itemId} className="opacity-0” />
<svg version="1.1" width="22" height="22" fill=“white" xmlns="http://www.w3.org/2000/svg” className="rounded-xl fill-white stroke-2 stroke-lightPurple">
<circle cx="11" cy="11" r="10" />
</svg>
<span className="sr-only">{`Mark ${name} as purchased`}</span>
</label>
<span>{name}</span>
And the visual result:
Now, let’s see how we can use the checkbox status to change the style applied to the SVG.
In Tailwind, every utility class can be applied conditionally by adding a modifier after the class name that describes the condition to target.
This way, I could add any styles based on the focus or checked status. But in this case, I don’t want to style the checkbox itself but the SVG, which is its sibling.
This is when the Tailwind peer
class becomes really handy. It allows us to simply mark the sibling with the peer class, and then use the peer-checked
or any modifiers to style the target element.
In the case of needing to style an element based on parent state, the group
class might be used instead of the peer
one.
The final code and UI will look like this:
<label className="flex items-center justify-center">
<input type="checkbox" id={itemId} className="peer opacity-0" />
<svg version=“1.1" width=“22" height=“22" xmlns="http://www.w3.org/2000/svg” fill=“white" className="rounded-xl fill-white stroke-2 stroke-lightPurple peer-focus:ring-2 ring-blue-900 peer-checked:fill-lightPurple" >
<circle cx="11" cy="11" r="10" />
</svg>
<span className="sr-only">{`Mark ${name} as purchased`}</span>
</label>
And this is the view of the resulting checkboxes:
At this point, I can just continue by adding the rest of styles to completely match the specifications:
If you found it useful and want to take a deeper look at the entire functionality, check out the complete code on GitHub.
Thanks for reading :)