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:

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:

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:

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.

Here’s how to customize Ant Design table row onClick, and icon button onClick.
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;