How to add Checkboxes to an Ant Design Select Component

The Ant Design Select component can have components rendered in its options list by adding them to the options label field. However, getting the clicks and event bubbling to play well together is challenging.

In this tutorial I will create the Select with Checkboxes seen below:

Ant Design Select with Checkboxes
Ant Design Select with Checkboxes

I used the default classes on the Select to make sure the checkboxes were not visible in the input area, and I also removed the default check marks to the right of the option.

Full code is in the Resources section.

Ant Design Select Options with Checkbox Components

The Select component accepts an array of options objects. Each object can have a value, label, and disabled field. The label field controls what is displayed in the dropdown and in the input area.

The label field can accept string values or components. Here is the code I used for my first option:

options={[
  {
    value: "john",
    label: (
      <Checkbox
        onClick={(e) => {
          if (dirty) { //I'll explain this later
            e.stopPropagation();
          }
          setDirty(false);
          console.log("Check Clicked");
        }}
        checked={selectedOptions.includes("john")}
      >
        John
      </Checkbox>
    )
  }
]}

This injected a Checkbox with text “John”. The checkbox appears to the left of the text. Ignore the “dirty” value for a moment, I’ll explain that later.

When the mode="multiple" prop is set on the Select, a checkmark appears to the right of the text and we don’t want that:

Ant Design Multi-Select checkmark
Ant Design Multi-Select checkmark

This can be removed by targeting the Ant Design classes used to create that checkmark. I set it to display: none.

.ant-select-dropdown .ant-select-item-option-state {
  display: none;
}

Here’s where I found that class in dev tools:

Remove check with ant-select-item-option-state
Remove check with ant-select-item-option-state

Ant Design Select with Multiple Options

I needed to make sure that the checkbox was selected no matter where the click target was on each option. In other words, if the click was on the blank space at the end of the option, I needed the checkbox to be checked because the item was added to the Select by that click.

First was creating the change handler for the Select:

const [selectedOptions, setSelectedOptions] = React.useState(["john"]);
const handleChange = (value: string[]) => {
    setSelectedOptions(value);
    setDirty(true);
    console.log("Select Changed");
  };

The value passed in was always the new array value, so I could swap out my selectedOptions with the new value. Interestingly, with TypeScript enabled I had to use ts-ignore because the Ant Design typing couldn’t handle the onChange value being an array instead of a string.

//@ts-ignore
onChange={handleChange}

Then in my checkbox checked prop, I checked if the selectedOptions array contained the relevant checkbox value:

checked={selectedOptions.includes("jim")}

If so, then the Checkbox would programmatically be selected. This kept the checkbox in sync with the Select component.

The MUI Select component can also have checkboxes injected.

Ant Design Select onChange, onClick, and Checkbox onClick

The biggest challenge was handling the Select onChange and the Checkbox onClick. Strangely, if I clicked the actual checkbox, then the Checkbox onClick fired first. If I clicked the text to the right of the checkbox, then Select onChange fired, then the checkbox onClick fired, which then once again fired the Select onChange, resulting in a duplicate and immediate reversal of the event.

I fixed this by adding a “dirty” state. If the Select handleChange had fired, then dirty was set to true. I also added an onMouseDown listener on the Select to reset the dirty state between clicks:

onMouseDown={(e) => {
  setDirty(false);
  console.log("Select Clicked");
}}

If dirty is true, then the Select has already updated its value and the checkbox should end propogation of the click event so the Select doesn’t fire twice.

This was really challenging to figure out. At first I tried to use the event.target in the checkbox, but whether I click the checkbox or the text the target had the same value. The workaround I created shouldn’t have been necessary.

Ant Design checkbox click event target
Ant Design checkbox click event target

Resources

Ant Design has a great table component with great column customizations.

//checkselect.css
.ant-select-selection-item .ant-checkbox {
  display: none;
}
.ant-select-dropdown .ant-select-item-option-state {
  display: none;
}

//checkselect.tsx
import React from "react";
import "antd/dist/antd.css";
import "./checkselect.css";
import { Checkbox, Select } from "antd";

const CheckSelect: React.FC = () => {
  const [selectedOptions, setSelectedOptions] = React.useState(["john"]);
  const [dirty, setDirty] = React.useState(false);
  const handleChange = (value: string[]) => {
    setSelectedOptions(value);
    setDirty(true);
    console.log("Select Changed");
  };
  return (
    <Select
      defaultValue="john"
      //@ts-ignore
      onChange={handleChange}
      onMouseDown={(e) => {
        setDirty(false);
        console.log("Select Clicked");
        e.stopPropagation();
      }}
      style={{ width: 150 }}
      mode="multiple"
      options={[
        {
          value: "john",
          label: (
            <Checkbox
              onClick={(e) => {
                if (dirty) {
                  e.stopPropagation();
                }
                setDirty(false);
                console.log("Check Clicked");
              }}
              checked={selectedOptions.includes("john")}
            >
              John
            </Checkbox>
          )
        },
        {
          value: "jim",
          label: (
            <Checkbox
              onClick={(e) => {
                if (dirty) {
                  e.stopPropagation();
                }
                setDirty(false);
                console.log("Check Clicked");
              }}
              checked={selectedOptions.includes("jim")}
            >
              Jim
            </Checkbox>
          )
        },
        {
          value: "johhny",
          label: (
            <Checkbox
              onClick={(e) => {
                if (dirty) {
                  e.stopPropagation();
                }
                setDirty(false);
                console.log("Check Clicked");
              }}
              checked={selectedOptions.includes("johhny")}
            >
              Johhny
            </Checkbox>
          )
        }
      ]}
    />
  );
};

export default CheckSelect;
Share this post:

Leave a Comment

This site uses Akismet to reduce spam. Learn how your comment data is processed.