Introduction
Dropdown menus are a common UI element, essential for navigation, filtering, or selecting options. While browsers provide native select elements, they often lack the styling flexibility and customizability needed for modern web designs. This is where building a custom dropdown in React comes in handy. In this post, we'll walk through the process of creating a reusable, accessible, and fully customizable dropdown component from scratch.
Why Custom Dropdowns?
- Design Flexibility: Achieve any visual style that native selects can't.
- Enhanced User Experience: Implement custom animations, search functionality, or multi-select options.
- Accessibility Control: Ensure your dropdown is usable by everyone, including those with assistive technologies.
Core Components of a Dropdown
A custom dropdown typically consists of:
- Trigger Button: The element that toggles the dropdown's visibility.
- Dropdown Content/Menu: The container for the list of options.
- Individual Options: The selectable items within the menu.
Building the React Component
1. Basic Structure (React)
Let's start with a basic functional React component. We'll use React's `useState` hook to manage the dropdown's open/closed state.
import React, { useState } from 'react';
const CustomDropdown = ({ options, onSelect, placeholder }) => {
const [isOpen, setIsOpen] = useState(false);
const [selectedOption, setSelectedOption] = useState(null);
const handleToggle = () => setIsOpen(!isOpen);
const handleSelect = (option) => {
setSelectedOption(option);
onSelect(option);
setIsOpen(false);
};
return (
<div className="relative">
<button
type="button"
onClick={handleToggle}
className="w-full px-4 py-2 text-left bg-white border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-2 focus:ring-purple-500 focus:border-transparent"
>
{selectedOption ? selectedOption.label : placeholder}
</button>
{isOpen && (
<div className="absolute z-10 w-full mt-1 bg-white rounded-md shadow-lg border border-gray-200">
<ul className="py-1">
{options.map((option) => (
<li
key={option.value}
onClick={() => handleSelect(option)}
className="px-4 py-2 hover:bg-gray-100 cursor-pointer"
>
{option.label}
</li>
))}
</ul>
</div>
)}
</div>
);
};
export default CustomDropdown;
2. Handling Outside Clicks
A crucial part of any dropdown is closing it when a user clicks outside. We can achieve this using `useEffect` and an event listener.
import React, { useState, useEffect, useRef } from 'react';
const CustomDropdown = ({ options, onSelect, placeholder }) => {
const [isOpen, setIsOpen] = useState(false);
const [selectedOption, setSelectedOption] = useState(null);
const dropdownRef = useRef(null); // Create a ref for the dropdown container
const handleToggle = () => setIsOpen(!isOpen);
const handleSelect = (option) => {
setSelectedOption(option);
onSelect(option);
setIsOpen(false);
};
useEffect(() => {
const handleClickOutside = (event) => {
if (dropdownRef.current && !dropdownRef.current.contains(event.target)) {
setIsOpen(false);
}
};
document.addEventListener('mousedown', handleClickOutside);
return () => {
document.removeEventListener('mousedown', handleClickOutside);
};
}, []); // Empty dependency array means this runs once on mount and cleans up on unmount
return (
<div className="relative" ref={dropdownRef}> {/* Attach the ref here */}
<button
type="button"
onClick={handleToggle}
className="w-full px-4 py-2 text-left bg-white border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-2 focus:ring-purple-500 focus:border-transparent"
>
{selectedOption ? selectedOption.label : placeholder}
</button>
{isOpen && (
<div className="absolute z-10 w-full mt-1 bg-white rounded-md shadow-lg border border-gray-200">
<ul className="py-1">
{options.map((option) => (
<li
key={option.value}
onClick={() => handleSelect(option)}
className="px-4 py-2 hover:bg-gray-100 cursor-pointer"
</li>
))}
</ul>
</div>
)}
</div>
);
};
export default CustomDropdown;
3. Accessibility (ARIA Attributes)
For a truly professional dropdown, accessibility is paramount. We need to add ARIA attributes to ensure screen readers and keyboard navigation work correctly.
import React, { useState, useEffect, useRef } from 'react';
const CustomDropdown = ({ options, onSelect, placeholder }) => {
const [isOpen, setIsOpen] = useState(false);
const [selectedOption, setSelectedOption] = useState(null);
const dropdownRef = useRef(null);
const handleToggle = () => setIsOpen(!isOpen);
const handleSelect = (option) => {
setSelectedOption(option);
onSelect(option);
setIsOpen(false);
};
useEffect(() => {
const handleClickOutside = (event) => {
if (dropdownRef.current && !dropdownRef.current.contains(event.target)) {
setIsOpen(false);
}
};
document.addEventListener('mousedown', handleClickOutside);
return () => {
document.removeEventListener('mousedown', handleClickOutside);
};
}, []);
return (
<div className="relative" ref={dropdownRef}>
<button
type="button"
onClick={handleToggle}
aria-haspopup="listbox"
aria-expanded={isOpen}
className="w-full px-4 py-2 text-left bg-white border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-2 focus:ring-purple-500 focus:border-transparent"
>
{selectedOption ? selectedOption.label : placeholder}
<!-- Optional: Add an arrow icon -->
<svg class="h-5 w-5 text-gray-400 absolute right-3 top-1/2 -translate-y-1/2" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true">
<path fillRule="evenodd" d="M5.293 7.293a1 1 0 011.414 0L10 10.586l3.293-3.293a1 1 0 111.414 1.414l-4 4a1 1 0 01-1.414 0l-4-4a1 1 0 010-1.414z" clipRule="evenodd" />
</svg>
</button>
{isOpen && (
<div
className="absolute z-10 w-full mt-1 bg-white rounded-md shadow-lg border border-gray-200"
role="listbox"
tabIndex="-1"
>
<ul className="py-1">
{options.map((option) => (
<li
key={option.value}
onClick={() => handleSelect(option)}
role="option"
aria-selected={selectedOption && selectedOption.value === option.value}
className="px-4 py-2 hover:bg-gray-100 cursor-pointer"
>
{option.label}
</li>
))}
</ul>
</div>
)}
</div>
);
};
export default CustomDropdown;
Key ARIA attributes used:
aria-haspopup="listbox"
on the button indicates it opens a listbox.aria-expanded
on the button indicates whether the dropdown is currently open.role="listbox"
on the dropdown content identifies it as a listbox.role="option"
on each list item identifies it as an option.aria-selected
on each option indicates if it's currently selected.
How to Use the CustomDropdown
You can use this component in your React application like this:
import React from 'react';
import CustomDropdown from './CustomDropdown'; // Assuming CustomDropdown.jsx
const App = () => {
const options = [
{ value: 'apple', label: 'Apple' },
{ value: 'banana', label: 'Banana' },
{ value: 'cherry', label: 'Cherry' },
];
const handleOptionSelect = (selectedOption) => {
console.log('Selected:', selectedOption);
// Do something with the selected option
};
return (
<div className="p-8 max-w-sm mx-auto">
<h1 className="text-2xl font-bold mb-4">My React App</h1>
<CustomDropdown
options={options}
onSelect={handleOptionSelect}
placeholder="Select a fruit"
/>
</div>
);
};
export default App;
Building a custom dropdown in React gives you full control over its appearance, behavior, and accessibility. By leveraging React's state management, `useEffect` for outside click detection, and crucial ARIA attributes, you can create robust and user-friendly dropdowns that seamlessly integrate with your application's design system. Experiment with different styles and functionalities to make your dropdowns truly unique!