DEV Community

GihanRangana
GihanRangana

Posted on

Highly Customizable React Custom Select Box Component

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>
Enter fullscreen mode Exit fullscreen mode

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');
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

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: '' }
Enter fullscreen mode Exit fullscreen mode
  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)
Enter fullscreen mode Exit fullscreen mode
  1. here, I'm using the useDebounce hook to handle the search input value
const _query = useDebounce(query, 150)
Enter fullscreen mode Exit fullscreen mode
  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])
Enter fullscreen mode Exit fullscreen mode
  1. use these functions to handle the select box values

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

    const handleFocus = () => {
        setIsOpen(true)
    }
Enter fullscreen mode Exit fullscreen mode
  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)
Enter fullscreen mode Exit fullscreen mode

use this wrapperRef as a ref for the wrapper div element

<div ref={wrapperRef} className={styles.wrapper}>
.......
</div>
Enter fullscreen mode Exit fullscreen mode

The demo code is as follows:

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

Top comments (0)