DEV Community

Uttam
Uttam

Posted on • Updated on

How to create a custom radio button and make it functional in react?

I was creating a product page for one of my e-commerce projects. And then I came into an issue of building a color and size selection component. I searched a lot on developer's best friend "Google" but could not find any optimal solution. After few exploration and testing I came up with this code.

Basic Setup

1.React
2.Tailwind

Preparing the data

Let's say we have an array of sizes for a particular product fetched from the db. We need to convert it to an object containing an id which will be used to map input to label and the value.

const productSize = ["S", "M", "X", "XXL"];
//map
const sizeData = productSize?.map((item) => ({
  id: `input_${item}`,
  value: item,
}));
Enter fullscreen mode Exit fullscreen mode

Defining States

I defined a state for tracking the state of radio values. If you are using multiple radio buttons , you should create multiple states

 let [sizeValue, setSizeValue] = useState("");
Enter fullscreen mode Exit fullscreen mode

Rendering the component

Map through the sizeData array and render each size. I am using tailwind css for styling which might make the code a bit messy.
Radio input is set to invisible so that the label is the clickable element. input id is mapped to label for.

<div className="main max-w-lg mx-auto">
      <div className="grid grid-cols-4 gap-8 gap-y-4">
        {sizeData?.map(({ id, value }) => (
          <div className="-mt-2" key={id}>
            <input
              id={id}
              className="invisible radio_custom"
              type="radio"
              value={value}
              checked={sizeValue === { sizeValue }}
              onChange={(e) => setSizeValue(e.target.value)}
            />
            <label htmlFor={id} className="radio_custom_label">
              <div
                className={`border border-gray-300 py-3 text-center cursor-pointer "
                        }`}
              >
                {value}
              </div>
            </label>
          </div>
        ))}
      </div>
    </div>
Enter fullscreen mode Exit fullscreen mode

Issues

Now, I have few issues with the above code. When a button is clicked , the other clicked buttons stays active. This doesn't give the toggle functionality that we require. The state is changing but the border on "onClick" persists. So even if the the state is changing, border stays on the element.
What we need is to remove the border from all element and add border to the "target" element.

onChange={(e) => {
    const nodes = e.target.parentElement.parentElement.childNodes;
    for (let i = 0; i < nodes.length; i++) {
    nodes[i].lastChild.firstChild.classList.remove("show_border");
    }
    e.target.nextSibling.firstChild.classList.toggle("show_border");
    return setSizeValue(e.target.value);
}}

Enter fullscreen mode Exit fullscreen mode

Final Code

I had to refactored the code like three timesπŸ™‚. Here is the final code.

import React, { useState } from "react";

const Main = () => {
  const productSize = ["S", "M", "X", "XXL"];
  //map
  const sizeData = productSize?.map((item) => ({
    id: `input_${item}`,
    value: item,
  }));
  let [sizeValue, setSizeValue] = useState("");
  console.log(sizeValue);
  return (
    <div className="main max-w-lg mx-auto">
      <div className="grid grid-cols-4 gap-8 gap-y-4">
        {sizeData?.map(({ id, value }) => (
          <div className="-mt-2" key={id}>
            <input
              id={id}
              className="invisible radio_custom"
              type="radio"
              value={value}
              checked={sizeValue === { sizeValue }}
              onChange={(e) => {
                const nodes = e.target.parentElement.parentElement.childNodes;
                for (let i = 0; i < nodes.length; i++) {
                  nodes[i].lastChild.firstChild.classList.remove("show_border");
                }
                e.target.nextSibling.firstChild.classList.toggle("show_border");
                return setSizeValue(e.target.value);
              }}
            />
            <label htmlFor={id} className="radio_custom_label">
              <div
                className={`border border-gray-300 py-3 text-center cursor-pointer "
                        }`}
              >
                {value}
              </div>
            </label>
          </div>
        ))}
      </div>
    </div>
  );
};

export default Main;
Enter fullscreen mode Exit fullscreen mode

I used tailwind so no need to add css code. Only extra one css class is required

.show_border {
  border: 1px solid #2b2b2b !important;
}
Enter fullscreen mode Exit fullscreen mode

Final Result

Final Notes

There are multiple ways of doing this. If you have any suggestion or code improvement please do share. Feel free to connect. It's lively to make new friends.πŸ˜€

Top comments (0)