When developing a website, customizing the select element in HTML is often not straightforward. Therefore, when building an application with React.js, you may need a customizable select component.
In this article, I will explain how to create a CustomSelect component in React.js.
Create React App
First, you can kickstart your React project by running the following command: npx create-react-app my-app
.
Let’s Start Coding The CustomSelect Component
First, let’s create a new folders named “components” and “assets” inside the “src” directory:
src/
components/
assets/
We will keep the components we use in our project inside the “components” folder, while we will store our style files in the “assets” folder.
Organizing our project in this manner allows us to manage our React components separately from our styling assets.
Now, let’s start coding the component:
Create new file named CustomSelect.js inside the component directory:
src/components/CustomSelect.js
import React from 'react'
const CustomSelect = () => {
return (
<div>CustomSelect</div>
)
}
export default CustomSelect
After creating our component, we proceed to create our initial style file and import it into the index.js file.
src/assets/custom-select.css
The updated index.js
file would look like this:
import React from 'react';
import ReactDOM from 'react-dom/client';
import './index.css';
import './assets/css/custom-select.css';
import App from './App';
import reportWebVitals from './reportWebVitals';
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
<React.StrictMode>
<App />
</React.StrictMode>
);
reportWebVitals();
In this modified index.js
file, we import the style file located in the "assets" folder using import './assets/css/custom-select.css';
. This allows you to apply styles to your React application.
We need to define some props for the CustomSelect component.
placeHolder (string): This prop is used to define the placeholder text that is displayed when no option is selected in the custom select component.
options (array of objects): The
options
prop is an array of objects representing the selectable options in the dropdown. Each object should have at least alabel
property to display the option text and avalue
property to uniquely identify the option.isMulti (boolean): The
isMulti
prop determines whether the custom select component allows multiple selections (true
) or only a single selection (false
).isSearchable (boolean): When set to
true
, theisSearchable
prop enables a search input field within the dropdown, allowing users to filter and search for options.onChange (function): The
onChange
prop is a callback function that is triggered when the selected value(s) in the custom select component change. It receives the updated value(s) as an argument and can be used to handle changes in the parent component.
import React from 'react'
const CustomSelect = ({ placeHolder, options, isMulti, isSearchable, onChange, align }) => {
return (
<div>CustomSelect</div>
)
}
export default CustomSelect
Now we need to make the necessary definitions for our component. We will use some React hooks in these definitions.
import React, { useEffect, useRef, useState } from "react";
// Icon component
const Icon = ({ isOpen }) => {
return (
<svg viewBox="0 0 24 24" width="18" height="18" stroke="#222" strokeWidth="1.5" fill="none" strokeLinecap="round" strokeLinejoin="round" className={isOpen ? 'translate' : ''}>
<polyline points="6 9 12 15 18 9"></polyline>
</svg>
);
};
// CloseIcon component
const CloseIcon = () => {
return (
<svg viewBox="0 0 24 24" width="14" height="14" stroke="#fff" strokeWidth="2" fill="none" strokeLinecap="round" strokeLinejoin="round">
<line x1="18" y1="6" x2="6" y2="18"></line><line x1="6" y1="6" x2="18" y2="18"></line>
</svg>
);
};
// CustomSelect component
const CustomSelect = ({ placeHolder, options, isMulti, isSearchable, onChange, align }) => {
// State variables using React hooks
const [showMenu, setShowMenu] = useState(false); // Controls the visibility of the dropdown menu
const [selectedValue, setSelectedValue] = useState(isMulti ? [] : null); // Stores the selected value(s)
const [searchValue, setSearchValue] = useState(""); // Stores the value entered in the search input
const searchRef = useRef(); // Reference to the search input element
const inputRef = useRef(); // Reference to the custom select input element
// JSX rendering...
return (
<div>CustomSelect</div>
);
};
export default CustomSelect;
In this code, I’ve added comments to explain the purpose of each state variable and how they are initialized using React hooks. These state variables help manage the behavior and appearance of the CustomSelect component, including the visibility of the dropdown menu, selected values, and the search input value. The useRef hooks are used to create references to DOM elements for later use in event handling.
Now it’s time to code the component’s functionality. The explanations of the functions I will write for the functionality of the component are as follows;
Icons
First, two components, namely Icon
and CloseIcon
, are defined. These components represent icons in SVG format and contain visual elements. The Icon
component is used to indicate whether the menu is open or closed, while the CloseIcon
component is used to remove selected items.
useRef
The component starts with several state variables and useRef
hooks. These state variables manage the behavior and appearance of the CustomSelect
component, including the visibility of the dropdown menu (showMenu
), selected values (selectedValue
), and the search input value (searchValue
). The searchRef
and inputRef
are used to create references to DOM elements for later use in event handling.
useEffect
Two useEffect
hooks are used to respond to lifecycle events of the component. The first useEffect
focuses on the search input when the showMenu
state changes and the menu is open. The second useEffect
listens for outside clicks to close the menu.
handleInputClick
The handleInputClick
function handles click events in the clickable area of the custom select box and toggles the menu's visibility.
getDisplay
The getDisplay
function returns the content to be displayed based on the selected value(s). If nothing is selected, it displays the placeholder text. In the case of multiple selections, it displays selected items and uses the "CloseIcon" to remove each item. Otherwise, it displays the selected value's label.
removeOption
The removeOption
function removes a specific option from the list of selected values.
onTagRemove
The onTagRemove
function is used to remove a tag (in multi-select mode) and updates the selected values by calling the onChange
function with the new value.
onItemClick
The onItemClick
function handles click events on an option. It updates the selected value(s) based on whether it's a multi-select or single-select mode and calls the onChange
function to update the selected values.
isSelected
The isSelected
function checks whether an option is selected. In multi-select mode, it searches among the selected items; in single-select mode, it checks the selected value.
onSearch
The onSearch
function updates the searchValue
state based on the text entered in the search input.
getOptions
The getOptions
function filters options based on the search query if the search box is in use.
Finally, the component returns JSX content, allowing users to see the customized selection box. When the dropdown menu is open, it displays the options and shows the search box if necessary.
import React, { useEffect, useRef, useState } from "react";
// Icon component
const Icon = ({ isOpen }) => {
return (
<svg viewBox="0 0 24 24" width="18" height="18" stroke="#222" strokeWidth="1.5" fill="none" strokeLinecap="round" strokeLinejoin="round" className={isOpen ? 'translate' : ''}>
<polyline points="6 9 12 15 18 9"></polyline>
</svg>
);
};
// CloseIcon component
const CloseIcon = () => {
return (
<svg viewBox="0 0 24 24" width="14" height="14" stroke="#fff" strokeWidth="2" fill="none" strokeLinecap="round" strokeLinejoin="round">
<line x1="18" y1="6" x2="6" y2="18"></line><line x1="6" y1="6" x2="18" y2="18"></line>
</svg>
);
};
// CustomSelect component
const CustomSelect = ({ placeHolder, options, isMulti, isSearchable, onChange, align }) => {
// State variables using React hooks
const [showMenu, setShowMenu] = useState(false); // Controls the visibility of the dropdown menu
const [selectedValue, setSelectedValue] = useState(isMulti ? [] : null); // Stores the selected value(s)
const [searchValue, setSearchValue] = useState(""); // Stores the value entered in the search input
const searchRef = useRef(); // Reference to the search input element
const inputRef = useRef(); // Reference to the custom select input element
useEffect(() => {
setSearchValue("");
if (showMenu && searchRef.current) {
searchRef.current.focus();
}
}, [showMenu]);
useEffect(() => {
const handler = (e) => {
if (inputRef.current && !inputRef.current.contains(e.target)) {
setShowMenu(false);
}
};
window.addEventListener("click", handler);
return () => {
window.removeEventListener("click", handler);
};
});
const handleInputClick = (e) => {
setShowMenu(!showMenu);
};
const getDisplay = () => {
if (!selectedValue || selectedValue.length === 0) {
return placeHolder;
}
if (isMulti) {
return (
<div className="dropdown-tags">
{
selectedValue.map((option, index) => (
<div key={`${option.value}-${index}`} className="dropdown-tag-item">
{option.label}
<span onClick={(e) => onTagRemove(e, option)} className="dropdown-tag-close" >
<CloseIcon />
</span>
</div>
))
}
</div>
);
}
return selectedValue.label;
};
const removeOption = (option) => {
return selectedValue.filter((o) => o.value !== option.value);
};
const onTagRemove = (e, option) => {
e.stopPropagation();
const newValue = removeOption(option);
setSelectedValue(newValue);
onChange(newValue);
};
const onItemClick = (option) => {
let newValue;
if (isMulti) {
if (selectedValue.findIndex((o) => o.value === option.value) >= 0) {
newValue = removeOption(option);
} else {
newValue = [...selectedValue, option];
}
} else {
newValue = option;
}
setSelectedValue(newValue);
onChange(newValue);
};
const isSelected = (option) => {
if (isMulti) {
return selectedValue.filter((o) => o.value === option.value).length > 0;
}
if (!selectedValue) {
return false;
}
return selectedValue.value === option.value;
};
const onSearch = (e) => {
setSearchValue(e.target.value);
};
const getOptions = () => {
if (!searchValue) {
return options;
}
return options.filter(
(option) =>
option.label.toLowerCase().indexOf(searchValue.toLowerCase()) >= 0
);
};
return (
<div className="custom--dropdown-container">
<div ref={inputRef} onClick={handleInputClick} className="dropdown-input">
<div className={`dropdown-selected-value ${!selectedValue || selectedValue.length === 0 ? 'placeholder' : ''}`}>{getDisplay()}</div>
<div className="dropdown-tools">
<div className="dropdown-tool">
<Icon isOpen={showMenu} />
</div>
</div>
</div>
{
showMenu && (
<div className={`dropdown-menu alignment--${align || 'auto'}`}>
{
isSearchable && (
<div className="search-box">
<input className="form-control" onChange={onSearch} value={searchValue} ref={searchRef} />
</div>
)
}
{
getOptions().map((option) => (
<div onClick={() => onItemClick(option)} key={option.value} className={`dropdown-item ${isSelected(option) && "selected"}`} >
{option.label}
</div>
))
}
</div>
)
}
</div>
);
}
It wouldn’t hurt to add some styling after the component’s codes are completed :)
Let’s Style
src/assets/css/custom-select.css
.custom--dropdown-container {
text-align: left;
border: 1px solid #ccc;
position: relative;
border-radius: 6px;
cursor: pointer;
width: -webkit-max-content;
width: -moz-max-content;
width: max-content;
}
.custom--dropdown-container .dropdown-input {
padding: 7px 11px;
display: -webkit-box;
display: -ms-flexbox;
display: flex;
-webkit-box-align: center;
-ms-flex-align: center;
align-items: center;
-webkit-box-pack: justify;
-ms-flex-pack: justify;
justify-content: space-between;
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
gap: 7px;
}
.custom--dropdown-container .dropdown-input .dropdown-selected-value.placeholder {
color: #82868b;
}
.custom--dropdown-container .dropdown-tool svg {
-webkit-transition: all 0.2s ease-in-out;
transition: all 0.2s ease-in-out;
}
.custom--dropdown-container .dropdown-tool svg.translate {
-webkit-transform: rotate(180deg);
-ms-transform: rotate(180deg);
transform: rotate(180deg);
}
.custom--dropdown-container .dropdown-menu {
width: -webkit-max-content;
width: -moz-max-content;
width: max-content;
padding: 5px;
position: absolute;
-webkit-transform: translateY(6px);
-ms-transform: translateY(6px);
transform: translateY(6px);
border: 1px solid #dbdbdb;
border-radius: 6px;
overflow: auto;
background-color: #fff;
z-index: 99;
max-height: 312px;
min-height: 50px;
}
.custom--dropdown-container .dropdown-menu::-webkit-scrollbar {
width: 5px;
}
.custom--dropdown-container .dropdown-menu::-webkit-scrollbar-track {
background: #f1f1f1;
}
.custom--dropdown-container .dropdown-menu::-webkit-scrollbar-thumb {
background: #888;
}
.custom--dropdown-container .dropdown-menu::-webkit-scrollbar-thumb:hover {
background: #555;
}
.custom--dropdown-container .dropdown-menu.alignment--left {
left: 0;
}
.custom--dropdown-container .dropdown-menu.alignment--right {
right: 0;
}
.custom--dropdown-container .dropdown-item {
padding: 7px 10px;
cursor: pointer;
-webkit-transition: background-color 0.35s ease;
transition: background-color 0.35s ease;
border-radius: 6px;
font-weight: 500;
}
.custom--dropdown-container .dropdown-item:hover {
background-color: rgba(130, 134, 139, 0.05);
color: #ff7300;
}
.custom--dropdown-container .dropdown-item.selected {
background-color: rgba(255, 115, 0, 0.075);
color: #ff7300;
font-weight: 600;
}
.custom--dropdown-container .search-box {
padding: 5px;
}
.custom--dropdown-container .search-box input {
width: 100%;
-webkit-box-sizing: border-box;
box-sizing: border-box;
padding: 5px;
border: 1px solid #ccc;
border-radius: 5px;
}
.custom--dropdown-container .dropdown-tags {
display: -webkit-box;
display: -ms-flexbox;
display: flex;
-ms-flex-wrap: wrap;
flex-wrap: wrap;
gap: 5px;
}
.custom--dropdown-container .dropdown-tag-item {
background-color: #ff7300;
color: #FFF;
font-size: 12px;
font-weight: 500;
padding: 2px 4px;
border-radius: 6px;
display: -webkit-box;
display: -ms-flexbox;
display: flex;
-webkit-box-align: center;
-ms-flex-align: center;
align-items: center;
}
.custom--dropdown-container .dropdown-tag-close {
display: -webkit-box;
display: -ms-flexbox;
display: flex;
-webkit-box-align: center;
-ms-flex-align: center;
align-items: center;
margin-left: 5px;
}
I added an example for you here. You can edit this style according to your needs.
Usage
Now you can use the CustomSelect component wherever you want.
I have shown an example usage for you below:
src/App.js
import { useState } from 'react';
import CustomSelect from './components/CustomSelect';
function App() {
const [options, setOptions] = useState([
{
label: "Option 1",
value: "opt1",
},
{
label: "Option 2",
value: "opt2",
},
{
label: "Option 3",
value: "opt3",
},
])
const handleChangeSelect = (e) => {
console.log(e)
}
return (
<div className="App">
<CustomSelect
options={options}
placeHolder='Please select...'
onChange={(e) => handleChangeSelect(e)}
isSearchable
isMulti
/>
</div>
);
}
export default App;
The coding for the CustomSelect component is now complete.
You can integrate this customizable selection component into your React applications to enhance user interactions.
If you’re interested in contributing to the development of a new React.js UI library, you’re welcome to fork the repository and submit pull requests at https://github.com/yagizmdemir/ymd-ui
If you’d like to access the source code or contribute to its development, you can find it on my GitHub repository: https://github.com/yagizmdemir/react-custom-select-component
Feel free to explore, use, and provide feedback or contributions.
Thank you for reading. Happy coding!