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>
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.
- 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: '' }
- 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)
- here, I'm using the useDebounce hook to handle the search input value
const _query = useDebounce(query, 150)
- 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])
- use these functions to handle the select box values
const handleValueChange = (option: SelectOption) => {
setValue(option)
setQuery('')
setIsOpen(false)
}
const handleFocus = () => {
setIsOpen(true)
}
- 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