Highly Customizable React Custom Select Box Component

Gihan RanganaGihan Rangana
4 min read

Enhance your user interfaces with a fully customizable React Select Box component. This versatile component replaces standard HTML select boxes with a user-friendly drop-down menu, giving you complete control over style, behavior, and functionality. Build intuitive and interactive forms that seamlessly integrate into your React applications, providing a superior user experience.

Let's get started!

First of all, we must create the HTML layout of our custom select box, Create Select.tsx file, and put this HTML into the react component return statement

<div className={styles.wrapper}>
<div className={styles.selectedContainer}>

                {!isOpen && <span className={styles.valueContainer}>{value.label}</span>}
                <input
                    title='"Select'
                    role='combobox'
                    ref={inputRef}
                    className={styles.inputContainer}
                    type='text'
                    value={query}
                    onChange={(e) => {
                        setQuery(e.target.value)
                    }}
                    placeholder={!value?.label ? placeholder : isOpen ? placeholder : ""}
                    readOnly={!isOpen}
                    onFocus={handleFocus}
                />
</div>

{isOpen && 

                    <div
                        className={[styles.optionsList, styles.top].join(' ')}
                    >
                            {filteredOptions.map((option) => (
                                <div
                                    key={option.value}
                                    className={styles.option}
                                    onClick={handleValueChange.bind(null, option)}
                                >
                                    {option.value === value.value &&
                                        <IoCheckmark />
                                    }

                                    {option.value !== value.value &&
                                        <div className={styles.emptyIcon} />
                                    }

                                    <span>{option.label}</span>
                                </div>
                            ))}
                    </div>
}

</div>

Create Selecta .module.scss file and put all these styles into the file

.wrapper {
    position: relative;
    border: 1px solid $ash-light;
    min-width: toRem(200);
    border-radius: map-get($map: $border-radius, $key: 'md');
    background-color: white;
    cursor: pointer;
}

.open {
    border-color: $primary;

    input {
        cursor: default !important;
    }
}

.selectedContainer {
    position: relative;
    display: flex;
    align-items: center;
    justify-content: center;

    .inputContainer {
        position: relative;
        padding: map-get($map: $padding, $key: 'md');
        font-size: map-get($map: $font-size, $key: 'md');
        box-sizing: border-box;
        cursor: default;
        z-index: 1;
        background-color: transparent;
        border: none;
        outline: none;
        flex: 1;
        width: 100%;
        padding-right: 25px;
        cursor: pointer;
    }

    .valueContainer {
        width: 100%;
        position: absolute;
        font-size: map-get($map: $font-size, $key: 'md');
        right: 0;
        left: map-get($map: $padding, $key: 'md');
        z-index: 0;
        pointer-events: none;
    }

    .expandIcon {
        padding: map-get($map: $padding, $key: 'md');
        border-left: 1px solid $ash-light;
        display: flex;
        align-items: center;
        height: 100%;
        color: $ash-dark;
    }

    .clearIcon {
        padding: map-get($map: $padding, $key: 'md');
        position: absolute;
        right: 35px;
        height: 100%;
        display: flex;
        align-items: center;
        color: $ash-dark;
        z-index: 1;
    }
}

.optionsList {
    position: absolute;
    margin-top: map-get($map: $margin, $key: 'sm');
    border-radius: map-get($map: $border-radius, $key: 'sm');
    border: 1px solid $ash-light;
    width: 100%;
    padding: calc(map-get($map: $padding, $key: 'md')/3);
    box-shadow: rgba(0, 0, 0, 0.1) 0px 4px 6px -1px, rgba(0, 0, 0, 0.06) 0px 2px 4px -1px;
    // max-height: 200px;
    // overflow: auto;

    &.top {
        top: 100%;
    }

    .option {
        padding: toRem(8) map-get($map: $padding, $key: 'sm');
        padding-left: map-get($map: $padding, $key: 'md');
        display: flex;
        align-items: center;
        cursor: default;
        font-size: map-get($map: $font-size, $key: 'md');
        border-radius: map-get($map: $border-radius, $key: 'sm');

        &:hover {
            background-color: lighten($color: $ash-light, $amount: 7%);
        }

        .emptyIcon {
            width: 14px;
            height: 14px;
        }

        span {
            margin-left: map-get($map: $margin, $key: 'sm');
        }
    }
}

Let's begin to handle the functionality.

  1. extracts the props on top of the component and creates selected value variable
const { defaultValue, options, placeholder, customStyles, clearable } = props;

    const selected: SelectOption = options.find(opt => opt.value === defaultValue) ?? { label: '', value: '' }
  1. Create react states and refs
    const [value, setValue] = useState<SelectOption>(selected)
    const [filteredOptions, setFilteredOptions] = useState<SelectOption[]>(options ?? [])
    const [isOpen, setIsOpen] = useState<boolean>(false)
    const [query, setQuery] = useState<string>('')

    const inputRef = useRef<HTMLInputElement>(null)
  1. here, I'm using the useDebounce hook to handle the search input value
const _query = useDebounce(query, 150)
  1. Create useEffect to filter the options based on the input value

    useEffect(() => {
        if (!_query) {
            setFilteredOptions(options)
            return
        }

        const regex = new RegExp(_query.toLowerCase(), 'g')
        const filtered = options.filter(opt => opt.label.toLowerCase().match(regex) ?? opt.value.toLowerCase().match(regex))
        setFilteredOptions(filtered)

    }, [_query, options])
  1. use these functions to handle the select box values

    const handleValueChange = (option: SelectOption) => {
        setValue(option)
        setQuery('')
        setIsOpen(false)
    }

    const handleFocus = () => {
        setIsOpen(true)
    }
  1. To close the dropdown when clicking outside of the component, I'm using the useClickOutside hook as follows
    const handleClickOutside = () => {
        setIsOpen(false)
    }
    const wrapperRef = useClickOutside<HTMLDivElement | null>(handleClickOutside)

use this wrapperRef as a ref for the wrapper div element

<div ref={wrapperRef} className={styles.wrapper}>
.......
</div>

The demo code is as follows:
https://stackblitz.com/edit/vitejs-vite-q2qbf8?file=src%2Fcomponents%2FSelect%2FSelect.tsx

Follow the above stackblitz demo to get a better understanding of that component. It has used framer-motion to animate the drop-down and simplebar-react used as a scrollbar and also react-icons

0
Subscribe to my newsletter

Read articles from Gihan Rangana directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Gihan Rangana
Gihan Rangana