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:

checkbox-complete

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:

checkbox-p1

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?