Styling checkboxes in React and Next with Tailwind

by Dan Edwards, 13 September 2024

Styling checkboxes in React and Next with Tailwind

In this article we'll be making little checkboxes like these:

I'll assume you've already got Tailwind up and running in your React/ Next.js project.

1. Install the Forms plugin.

...if you haven't already. This is required because Tailwind doesn't simply apply CSS classes to the check boxes - it replaces them with an SVG.

Command line
pnpm add @tailwindcss/forms

# Or
npm i @tailwindcss/forms

2. Import the Forms plugin.

tailwind.config.ts
TypeScript
1import forms from '@tailwindcss/forms';
2import { type Config } from 'tailwindcss';
3
4export default {
5	content: ['./src/**/*.{ts,tsx}'],
6	plugins: [forms],
7} satisfies Config;

3. Create a Checkbox component

This creates a functional checkbox in a nice default blue colour. I've used the clsx(opens in a new tab) package, which is a lovely little utility for keeping your class names organised, especially when you've got conditional logic involved.

Checkbox.tsx
TypeScript
1'use client'; // You only need this line in Next.js, not React
2import { ReactNode, useState, useEffect } from 'react';
3import clsx from 'clsx';
4
5export default function Checkbox({
6	checked = false,
7	onChange,
8	children,
9}: CheckboxProps) {
10	const [isChecked, setIsChecked] = useState(checked);
11
12	useEffect(() => {
13		setIsChecked(checked);
14	}, [checked]);
15
16	const handleChange = () => {
17		const newChecked = !isChecked;
18		setIsChecked(newChecked);
19		if (onChange) {
20			onChange(newChecked);
21		}
22	};
23
24	return (
25		<div className="flex items-center me-2">
26			<input
27				type="checkbox"
28				id={`checkbox-${children}`}
29				checked={isChecked}
30				onChange={handleChange}
31				className={clsx(
32					'w-6 h-6',
33					'bg-gray-100',
34					'border-gray-300',
35					'rounded',
36					'focus:ring-2',
37					'transition duration-150 ease-in-out',
38				)}
39			/>
40			<label className="text-sm ms-2" htmlFor={`checkbox-${children}`}>
41				{children}
42			</label>
43		</div>
44	);
45}

4. Add some colours

Now we can add some colours to the checkboxes, though some of these are more confusing than you might think.

Unchecked background colour

This is simply the regular background property. I'm using bg-gray-100

Add a subtle hover style to indicate that it's interactive, such as bg-red-200

Checked style

To change the colour of the negative space around the check mark, use a text colour style, such as text-red-500

The tick shape is actually an SVG that Tailwind injects for you, so if you want to change the colour it's quite complicated. I haven't bothered as they look pretty cool with white.

Focus ring

Finally, change the colour of the focus ring with something like focus:ring-red-400

5. Create colour options

Now let's create a colour map with these options to keep our code organised.

Checkbox.tsx
TypeScript
1const colourMap = {
2	red: 'text-red-500 focus:ring-red-400 hover:bg-red-200',
3	orange: 'text-orange-500 focus:ring-orange-400 hover:bg-orange-200',
4	yellow: 'text-yellow-500 focus:ring-yellow-400 hover:bg-yellow-200',
5	green: 'text-green-500 focus:ring-green-400 hover:bg-green-200',
6	blue: 'text-blue-500 focus:ring-blue-400 hover:bg-blue-200',
7	indigo: 'text-indigo-500 focus:ring-indigo-400 hover:bg-indigo-200',
8	violet: 'text-violet-500 focus:ring-violet-400 hover:bg-violet-200',
9};

Then we'll add a union type to the Checkbox props, which will allow our IDE to display the list of options as we're calling the component.

Checkbox.tsx
TypeScript
1interface CheckboxProps {
2	colour: 'red' | 'orange' | 'yellow' | 'green' | 'blue' | 'indigo' | 'violet';
3	checked?: boolean;
4	onChange?: (checked: boolean) => void;
5	children: ReactNode;
6}

6. The code in full

That's it! Here's the component in full:

Checkbox.tsx
TypeScript
1'use client';
2import { ReactNode, useState, useEffect } from 'react';
3import clsx from 'clsx';
4
5interface CheckboxProps {
6	colour: 'red' | 'orange' | 'yellow' | 'green' | 'blue' | 'indigo' | 'violet';
7	checked?: boolean;
8	onChange?: (checked: boolean) => void;
9	children: ReactNode;
10}
11
12const colourMap = {
13	red: 'text-red-500 focus:ring-red-400 hover:bg-red-200',
14	orange: 'text-orange-500 focus:ring-orange-400 hover:bg-orange-200',
15	yellow: 'text-yellow-500 focus:ring-yellow-400 hover:bg-yellow-200',
16	green: 'text-green-500 focus:ring-green-400 hover:bg-green-200',
17	blue: 'text-blue-500 focus:ring-blue-400 hover:bg-blue-200',
18	indigo: 'text-indigo-500 focus:ring-indigo-400 hover:bg-indigo-200',
19	violet: 'text-violet-500 focus:ring-violet-400 hover:bg-violet-200',
20};
21
22export default function Checkbox({
23	colour,
24	checked = false,
25	onChange,
26	children,
27}: CheckboxProps) {
28	const [isChecked, setIsChecked] = useState(checked);
29
30	useEffect(() => {
31		setIsChecked(checked);
32	}, [checked]);
33
34	const handleChange = () => {
35		const newChecked = !isChecked;
36		setIsChecked(newChecked);
37		if (onChange) {
38			onChange(newChecked);
39		}
40	};
41
42	return (
43		<div className="flex items-center me-2">
44			<input
45				type="checkbox"
46				id={`checkbox-${children}`}
47				checked={isChecked}
48				onChange={handleChange}
49				className={clsx(
50					'w-6 h-6',
51					'bg-gray-100',
52					'border-gray-300',
53					'rounded',
54					'focus:ring-2',
55					'transition duration-150 ease-in-out',
56					colourMap[colour]
57				)}
58			/>
59			<label className="text-sm ms-2" htmlFor={`checkbox-${children}`}>
60				{children}
61			</label>
62		</div>
63	);
64}

And here's how you'd call it from another page/ component:

page.tsx
TypeScript
1import Checkbox from './Checkbox';
2
3export default function Page() {
4	return <Checkbox colour="green">Tick me</Checkbox>;
5}