Photo by Thomas Bormans on Unsplash
Building a Checkbox
UI Kit with React, TypeScript, Storybook and Tailwind
Introduction
This is part of the UI Kit with React, TypeScript, Storybook and Tailwind
, make sure you've checked out the previous entries in the series if you find anything confusing 😊.
In this series I aim to implement a basic text field and integrate it with Storybook
.
The Checkbox
Let's start off by creating what's necessary for the checkbox. I want my text field to support two different styles, one checkmark and one cross. It should support being disabled
, support focus for keyboard accessibility, and have visible transitions between states. On top of that I want it to integrate with react-hook-form
as that is one of my favorite form libraries for React
.
In the end, I want it to look like this:
In the project, create the files:
src/components/Checkbox/Checkbox.tsx
src/stories/Checkbox/Checkbox.stories.tsx
First, we'll create the necessary structure for the Checkbox
:
// TextField.tsx
import { Control, FieldValues, Path } from "react-hook-form";
export type CheckboxProps<T extends FieldValues> = {
type?: CheckboxType;
control: Control<T>;
name: Path<T>;
label?: string;
isDisabled?: boolean;
};
export enum CheckboxType {
CROSS = "cross",
CHECK = "check",
}
const Checkbox = <T extends FieldValues>({
name,
label = "",
control,
isDisabled = false,
type = CheckboxType.CHECK,
}: CheckboxProps<T>) => {
return <input type="checkbox" />;
};
export default Checkbox;
That's the basics of it, but now all we have a simple ordinary checkbox that looks kind of boring, has no label and it is not hooked up through react-hook-form
.
Adding the Label
Let's follow a similar approach to how the label was handled in the TextField
component from a previous post in this series.
Add the following to Checkbox.tsx
:
// Checkbox.tsx
const Checkbox = <T extends FieldValues>({
name,
label = "",
control,
isDisabled = false,
type = CheckboxType.CHECK,
}: CheckboxProps<T>) => {
return (
<label className={"flex items-center"}>
<span className="mr-2.5 text-sm">{label}</span>
<input type="checkbox" />
</label>
);
};
At least we have a label now, but let's tackle styling the checkbox itself now.
Adding Custom Styles
Since I want my checkbox to support different types of styles, like a checkmark and a cross, let's create a function that can make these for us. I won't go into detail on how this works specifically, there's a lot of fiddling around with values until you get what you're after. Here's the function:
// Checkbox.tsx
const getCheckboxType = (type: CheckboxType) => {
switch (type) {
case CheckboxType.CROSS:
return (
<>
<div
className="absolute rounded-md bg-white"
style={{
width: "100%",
height: "25%",
top: "40%",
transform: "rotate(45deg)",
}}
></div>
<div
className="absolute rounded-md bg-white"
style={{
width: "100%",
height: "25%",
top: "40%",
transform: "rotate(135deg)",
}}
></div>
</>
);
case CheckboxType.CHECK:
return (
<>
<div
className="absolute bg-white rounded-md"
style={{
width: "50%",
height: "25%",
transform: "rotate(35deg)",
left: "6%",
bottom: "15%",
}}
></div>
<div
className="absolute bg-white rounded-md"
style={{
width: "90%",
height: "25%",
transform: "rotate(125deg)",
left: "18%",
bottom: "35%",
}}
></div>
</>
);
}
};
And use it in the component like so:
// Checkbox.tsx
const Checkbox = <T extends FieldValues>({
name,
label = "",
control,
isDisabled = false,
type = CheckboxType.CHECK,
}: CheckboxProps<T>) => {
return (
<label className={"flex items-center"}>
<span className="mr-2.5 text-sm">{label}</span>
<div
className="relative ring-indigo-700 ring"
style={{ width: "0.8em", height: "0.8em" }}
>
<div className={"w-full h-full transition-all bg-indigo-700"}>
{getCheckboxType(type)}
<input className="opacity-0" type="checkbox" />
</div>
</div>
</label>
);
};
Note that the regular input field is now hidden
(by saying opacity-0
it is fully transparent). It is important to keep it in the DOM. If we hide it with for example display: none;
we lose the ability to focus the field and we also lose built-in keyboard controls.
At this point the checkbox looks like this:
That's a bit problematic, it's currently stuck looking like it's checked.
Integrating react-hook-form
I talked more about react-hook-form
and cx
(classnames
) which will both get in the following updates to the code, visit those posts to learn more or read up on their documentation.
Similar to how we integrated the TextField
from a previous post with react-hook-form
we will be following a similar approach here but this one is slightly different.
As can be seen earlier in the post, we define a type on our props called Control
, this is a type from react-hook-form
that we need in our component.
To integrate the component with react-hook-form
, the component looks like this:
// Checkbox.tsx
const Checkbox = <T extends FieldValues>({
name,
label = "",
control,
isDisabled = false,
type = CheckboxType.CHECK,
}: CheckboxProps<T>) => {
return (
<Controller
control={control}
render={({ field }) => {
return (
<label
className={cx("flex items-center", { "opacity-50": isDisabled })}
>
<span className="mr-2.5 text-sm">{label}</span>
<div
className="relative ring-indigo-700 ring"
style={{ width: "0.8em", height: "0.8em" }}
>
<div
className={cx("w-full h-full transition-all", {
"hover:bg-indigo-200": !field.value && !isDisabled,
"hover:bg-indigo-900": field.value && !isDisabled,
"bg-indigo-700": field.value,
"focus-within:ring-4 focus-within:ring-indigo-400":
!isDisabled,
})}
>
{field.value && getCheckboxType(type)}
<input
ref={field.ref}
name={field.name}
checked={field.value}
onChange={(...args) => {
if (isDisabled) return;
field.onChange(...args);
}}
className="opacity-0"
type="checkbox"
/>
</div>
</div>
</label>
);
}}
name={name}
/>
);
};
Instead of using register
which we used for our TextField
component, we use the Controller
component from react-hook-form
this time. By doing this, we can get the value of the current field inside the component itself, available on the field
render prop.
The rest of the code doesn't contain too many new things, you might recognize the use of cx
again to combine classes based on properties in the class. You'll also notice how focus-within
is used rather than focus
, this is because we want to apply those styles when the focus is actually on the invisible input
field. We now also utilize the isDisabled
property to be able to prevent the field from being interacted with when it is disabled.
Now our component is integrated with react-hook-form
and we are ready to create some stories in Storybook
.
Adding the Stories
In the file we created earlier, src/stories/Checkbox/Checkbox.stories.tsx
, place the following:
// Checkbox.stories.tsx
import { ComponentMeta, Story } from "@storybook/react";
import { FieldValues, useForm, useWatch } from "react-hook-form";
import Checkbox, {
CheckboxProps,
CheckboxType,
} from "../../components/Checkbox/Checkbox";
export default {
title: "Checkbox",
component: Checkbox,
} as ComponentMeta<typeof Checkbox>;
const Template: Story<CheckboxProps<FieldValues>> = (args) => {
const form = useForm();
return <Checkbox {...args} name="test" control={form.control} />;
};
export const Basic = Template.bind({});
Basic.args = { label: "Name" };
export const Disabled = Template.bind({});
Disabled.args = { label: "Name", isDisabled: true };
export const InForm = () => {
const { control, getValues } = useForm({
mode: "onChange",
defaultValues: { cheese: true, onions: true, mayo: false },
});
useWatch({ control, name: "cheese" });
useWatch({ control, name: "onions" });
useWatch({ control, name: "mayo" });
return (
<div className="w-56">
<div className="flex gap-4">
<Checkbox
name="cheese"
label="Cheese"
control={control}
type={CheckboxType.CHECK}
/>
<Checkbox
name="onions"
label="Onions"
control={control}
type={CheckboxType.CROSS}
/>
<Checkbox
name="mayo"
label="Mayo"
control={control}
type={CheckboxType.CROSS}
/>
</div>
<pre>
<code>{JSON.stringify(getValues(), null, 2)}</code>
</pre>
</div>
);
};
Running Storybook
To see how this actually looks in reality, we'll need to start our Storybook
by running yarn storybook
.
Run Storybook
, visit localhost:6006 and you'll find the checkbox component in the sidebar.
Troubleshooting
If you can't get it to run, or have some other concerns, check it out from my repo and see if that works:
github.com/simon-nystrom/newcurrent-react-u..
Stay tuned for the next entry in the series 😊. What component would you like to see next?