ReactHustle

How to Create Custom Formik Radio Button / Radio Group (with Typescript)

Jasser Mark Arioste

Jasser Mark Arioste

How to Create Custom Formik Radio Button / Radio Group (with Typescript)

Hello, hustlers! In this tutorial, you'll learn how to create a custom accessible formik radio button component using HeadlessUI and TailwindCSS! We'll also implement it with Typescript.

What is HeadlessUI? #

HeadlessUI is a set of fully unstyled and accessible components for React or Vue. Since they're already accessible, we don't have to implement features like keyboard functionality. And since they're unstyled, we have complete control of the styling. It's designed to be easily integrated with Tailwind CSS as well.

What is Tailwind CSS? #

Tailwind CSS is a set of utility CSS classes to style your HTML components. 

Why use HeadlessUI + TailwindCSS? #

We're using these two libraries to have complete customizability for our components.

I like using Tailwind CSS since CSS Classes can be used anywhere, so it's effortless to integrate Tailwind into new or existing projects. Using the tailwind VSCode extension, we have complete intellisense and documentation inside VScode without going to the website. It also keeps the CSS bundle size to a minimum since it only bundles the classes used in the code base.

This makes it very easy to style components in general and results in a fast development time.

Step 0 - Project Setup & Installing the Dependencies #

If you already have tailwindcss or headlessui installed for your project, you may proceed to Step 2.

For this tutorial, I'm using NextJS as our react framework. In my experience, NextJS + TailwindCSS is such a joy to work with. If you're using a different framework in your project, make sure you check the tailwind's installation docs

First, let's create a brand new NextJS project:

npx create-next-app --ts formik-custom-radio-button-tutorial

Once that's done, let's install tailwindcssheadlessui/react and formik packages all in one go. We'll also use the classnames package to manage the classes later on.

# npm
npm install -D tailwindcss postcss autoprefixer 
npm install --save headlessui/react formik classnames

#yarn
yarn add -D tailwindcss postcss autoprefixer 
yarn add @headlessui/react formik classnames

Initialize tailwind to create the tailwind.config.js and postcss.config.js files:

npx tailwindcss init

Add the .tsx template paths to tailwind.config.js:

/** @type {import('tailwindcss').Config} */
module.exports = {
  content: [
    "./app/**/*.{js,ts,jsx,tsx}",
    "./pages/**/*.{js,ts,jsx,tsx}",
    "./components/**/*.{js,ts,jsx,tsx}",
  ],
  theme: {
    extend: {},
  },
  plugins: [],
}
123456789101112

Lastly, modify styles/globals.css to add tailwind directives:

@tailwind base;
@tailwind components;
@tailwind utilities;
123

Now, we can proceed to create a custom radio button component.

Step 1 - Creating a Custom <RadioGroup/> Component #

Let's create a custom radio group component, that works without Formik. Since the value of a selected item in a radio group can be of any type, let's create a generic component that can hold any value.

First, create the file components/CustomRadioGroup.tsx and add the following code:

import React from "react";
export type CustomRadioGroupOption<TValue> = {
  label: string;
  value: TValue;
};
export type CustomRadioGroupProps<TValue> = {
  value: TValue;
  onChange(newVal: TValue): void;
  options: CustomRadioGroupOption<TValue>[];
};
// create a generic component definition that accepts any kind of value
const CustomRadioGroup = <TValue,>(props: CustomRadioGroupProps<TValue>) => {
  return <div>CustomRadioGroup</div>;
};
export default CustomRadioGroup;
123456789101112131415

Now if we use this component provided with CustomRadioGroupOption that has a number value, we get the automatic type-safety:

import CustomRadioGroup from "components/CustomRadioGroup";
import React from "react";
const HomePage = () => {
  return (
    <CustomRadioGroup
      value={null}
      onChange={(val) => console.log(val)} // (parameter) val: number
      options={[
        {
          label: "option1",
          value: 1,
        },
        {
          label: "option2",
          value: 2,
        },
      ]}
    ></CustomRadioGroup>
  );
};
export default HomePage;

123456789101112131415161718192021

This makes sure that we don't set a value that's of type string or add an option that has a string value.

Next, let's make the component functional using the headlessui/RadioGroup component:

import React from "react";
import { RadioGroup } from "@headlessui/react";
import classNames from "classnames";
...
// create a generic component definition that accepts any kind of value
const CustomRadioGroup = <TValue,>(props: CustomRadioGroupProps<TValue>) => {
  return (
    // use the value and onChange from props.
    <RadioGroup value={props.value} onChange={props.onChange}>
      <RadioGroup.Label className="text-lg my-2">
        {props.label}
      </RadioGroup.Label>
      {/* render each option. */}
      <div className="flex flex-col gap-2">
        {props.options.map((option) => {
          return (
            <RadioGroup.Option value={option.value} key={option.label}>
              {/* Use renderProps to get the checked state for each option. */}
              {/* Render the state appropriately using tailwind classes */}
              {({ checked }) => (
                <div
                  className={classNames({
                    "flex gap-2 rounded-md px-2 py-1 border-2 cursor-pointer":
                      true,
                    "outline outline-1": checked,
                  })}
                >
                  <span className="w-5 h-5">
                    {checked ? <span>✔️</span> : <span>⭕</span>}
                  </span>
                  <RadioGroup.Label>{option.label}</RadioGroup.Label>
                </div>
              )}
            </RadioGroup.Option>
          );
        })}
      </div>
    </RadioGroup>
  );
};
12345678910111213141516171819202122232425262728293031323334353637383940

It's totally up to you how you design your <RadioGroup.Option/> component. The code provided above is just an example. If you want to know more about the component, you may read the documentation as they provide examples.

After this step, we have something like this. Mouse and keyboard control just work out of the box:

React Custom Radio Group using HeadlessUI and Tailwind CSS.

Step 2 - Creating a Custom FormikRadioGroup Component #

Now that the hard part is done, let's integrate formik by creating a FormikRadioGroup component. We'll use the combination of useField and setFieldValue to modify the Formik context state.

Create the file components/FormikRadioGroup.tsx:

// components/FormikRadioGroup.tsx
import { useField, useFormikContext } from "formik";
import React from "react";
import CustomRadioGroup, { CustomRadioGroupOption } from "./CustomRadioGroup";
type FormikRadioGroupProps<TValue> = {
  name: string;
  options: CustomRadioGroupOption<TValue>[];
  label: string;
};
const FormikRadioGroup = <TValue,>(props: FormikRadioGroupProps<TValue>) => {
  const [field] = useField<TValue>(props.name);
  const { setFieldValue } = useFormikContext();
  return (
    <CustomRadioGroup
      options={props.options}
      // use field.value for value
      value={field.value}
      label={props.label}
      onChange={(val) => {
        // use setFieldValue to modify the formikContext
        setFieldValue(props.name, val);
      }}
    />
  );
};
export default FormikRadioGroup;
1234567891011121314151617181920212223242526

Explanation:

Lines 6-11: We define the component and component Props with a generic type. Since CustomRadioGroup is a generic component we should also create a generic component.

Line 18: We set the value of the CustomRadioGroup component using the field.

Line 22: We modify the formik context using setFieldValue.

In my opinion, this is one of the best ways (if not the best) to create custom reusable formik components from regular components.

Step 3 - Usage #

To use this component, we should use the <Formik/> provider and have to add the name property to <FormikRadioGroup /> component.

import FormikRadioGroup from "components/FormikRadioGroup";
import { Form, Formik } from "formik";
import React from "react";
const HomePage = () => {
  return (
    // initialize formik here
    <Formik
      onSubmit={(values) => console.log(JSON.stringify(values))}
      initialValues={{
        myoption: null,
      }}
    >
      <Form className="w-fit m-4">
        <FormikRadioGroup
          name="myoption" //add name props
          label="My Radio Group"
          options={[
            {
              label: "option 1",
              value: 1,
            },
            {
              label: "option 2",
              value: 2,
            },
            {
              label: "option 3",
              value: 3,
            },
          ]}
        ></FormikRadioGroup>
        <button type="submit">Submit</button>
      </Form>
    </Formik>
  );
};
export default HomePage;

12345678910111213141516171819202122232425262728293031323334353637

When we click submit, the value gets logged in the console:

{"myoption":2}
1

Full Code + Demo #

The full code is publicly available on my GitHub: https://github.com/jmarioste/formik-custom-radio-button-tutorial.

The demo is available on Stackblitz: Formik Custom Radio Group Tutorial

Conclusion #

We learned how to integrate Formik with another HeadlessUI and were able to customize the component to meet our requirements. HeadlessUI and Tailwind CSS are both amazing libraries for creating custom and accessible components and I think should be really good to learn more about them.

If you like this tutorial, please leave a like or share this article. For future tutorials like this, please subscribe to our newsletter or follow me on Twitter.

Credits: Image by David Mark from Pixabay

Share this post!

Related Posts

Disclaimer

This content may contain links to products, software and services. Please assume all such links are affiliate links which may result in my earning commissions and fees.
As an Amazon Associate, I earn from qualifying purchases. This means that whenever you buy a product on Amazon from a link on our site, we receive a small percentage of its price at no extra cost to you. This helps us continue to provide valuable content and reviews to you. Thank you for your support!
Donate to ReactHustle