Highly Customizable React Custom Select Box Component
Tags: react, reactselect, vite, customcomponent
Published: 2024-06-15 16:44:47

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

```html
<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

```scss
.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
```tsx
const { defaultValue, options, placeholder, customStyles, clearable } = props;

    const selected: SelectOption = options.find(opt => opt.value === defaultValue) ?? { label: '', value: '' }
```

2. Create react states and refs
```tsx
    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)
```

3. here, I'm using the useDebounce hook to handle the search input value
```tsx
const _query = useDebounce(query, 150)
```

4. Create useEffect to filter the options based on the input value
```tsx

    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])
```

5. use these functions to handle the select box values
```tsx

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

    const handleFocus = () => {
        setIsOpen(true)
    }
```

6. To close the dropdown when clicking outside of the component, I'm using the useClickOutside hook as follows
```tsx
    const handleClickOutside = () => {
        setIsOpen(false)
    }
    const wrapperRef = useClickOutside<HTMLDivElement | null>(handleClickOutside)
```
use this `wrapperRef` as a ref for the wrapper div element
```tsx
<div ref={wrapperRef} className={styles.wrapper}>
.......
</div>
```

The demo code is as follows: 
{% embed 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`