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.

1_2i5_5MgjD0nFX8i04xajRQ.webp

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.

  1. placeHolder (string): This prop is used to define the placeholder text that is displayed when no option is selected in the custom select component.

  2. 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 a label property to display the option text and a value property to uniquely identify the option.

  3. isMulti (boolean): The isMulti prop determines whether the custom select component allows multiple selections (true) or only a single selection (false).

  4. isSearchable (boolean): When set to true, the isSearchable prop enables a search input field within the dropdown, allowing users to filter and search for options.

  5. 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!