Building a Custom Dropdown Component in React (Step by Step)✨

Kader Biral
7 min readMar 7, 2024

--

In this article, I will introduce a custom Dropdown component that you can use dynamically in React applications. This Dropdown component allows users to select an item from a list while being able to displayed in different positions and styles. It can also be used with or without a image. The component is developed using popular libraries such as Typescript, Tailwind CSS, React Icons and Classnames.

👉 First I will explain what is happening step by step and at he end I will add the full code and its usage. If you’re ready, let’s start!

Overview

1. Imports:

import { useEffect, useRef, useState } from "react";
import classNames from "classnames";
import { GoChevronDown } from "react-icons/go";
import useOutsideClick from "../hooks/useOutsideClick";

🔹useEffect , useRef and useState➜ These are React hooks used for managing state and creating references to DOM elements.

🔹classnames➜ This library is used to conditionally join classNames together.

🔹goChevronDown➜ This is an icon component used for the dropdown toggle button.

🔹useOutsideClick➜ This is a custom hook used to detect clicks outside of a specified element.

2. Interfaces:

interface DropdownItem {
id: string;
name: string;
imageUrl?: string;
}

interface DropdownProps {
id: string;
title?: string;
data: DropdownItem[];
position?: "bottom-right" | "bottom-left" | "top-right" | "top-left";
hasImage?: boolean;
style?: string;
selectedId?: string;
onSelect?: (id: string) => void;
}

🔹DropdownItem➜ This defines an interface called DropdownItem. This interface specifies the properties required foe each dropdownitem: id, name and optionally imageUrl.

🔹DropdownProps➜ This interface specifies the props that can be passed to the Dropdown component. Description of these props;

Different Positions

📌 id : The unique identifier for the Dropdown component.

📌 title : The default title displayed before the dropdown is opened. The default title is “Select”.

📌 data : A list of items that make up the content of the dropdown.

📌 position : The position of the dropdown relative to the button.(“bottom-right”, “bottom-left”, “top-right”, “top-left”). The default position is “bottom-left”.

📌 hasImage : A boolean value indicating whether the items in the dropdown have images associated with them.

📌 style : Additional styles to be applied to the Dropdown component.

📌 selectedId : The identifier of the initially selected item.

📌 onSelect : A callback function invoked when an item is selected.

Customizable Styles

These props allow for the customization of the Dropdown component to fit different use cases, enabling users to tailor the behavior, content, and appearance of the dropdown according to their needs.

3. Dropdown Component Creation:

const Dropdown = ({
id,
title = "Select", //Default value
data,
position = "bottom-left", //Default value
hasImage,
style,
selectedId,
onSelect,
}: DropdownProps) => {
return (
<div ref={dropdownRef} className='relative'>
{/* ... */}
</div>
);
};
export default Dropdown;

🔹The Dropdown component is a custom dropdown menu component and accepts the above props.

4. State Variables:

const [isOpen, setIsOpen] = useState<boolean>(false);

const [selectedItem, setSelectedItem] = useState<DropdownItem | undefined>(
selectedId ? data?.find((item) => item.id === selectedId) : undefined
);

🔹isOpen➜ Manages the open/close state of the dropdown menu.

🔹selectedItem➜ Tracks the currently selected item in the dropdown menu.

5. Event Handler:

  const handleChange = (item: DropdownItem) => {
setSelectedItem(item);
onSelect && onSelect(item.id);
setIsOpen(false);
};

🔹handleChange➜ Handles the selection of an item from the dropdown menu. It updates the selected item, calls the onSelect callback (if provided), and closes the dropdown menu.

6. useEffect Hook:

useEffect(() => {
if (selectedId && data) {
const newSelectedItem = data.find((item) => item.id === selectedId);
newSelectedItem && setSelectedItem(newSelectedItem);
} else {
setSelectedItem(undefined);
}
}, [selectedId, data]);

🔹This useEffect hook listens for changes to selectedId and data . When one of them changes, it finds the item in the data that matches the selectedId and sets it to selectedItem.

7. Ref:

const dropdownRef = useRef<HTMLDivElement>(null);
useOutsideClick({
ref: dropdownRef,
handler: () => setIsOpen(false),
});

🔹dropdownRef➜ Ref used to detect clicks outside of the dropdown menu.

8. CSS Classes:

const dropdownClass = classNames(
'absolute bg-gray-100 w-max max-h-52 overflow-y-auto py-3 rounded shadow-md z-10',
{
'top-full right-0 mt-2': position === 'bottom-right',
'top-full left-0 mt-2': position === 'bottom-left',
'bottom-full right-0 mb-2': position === 'top-right',
'bottom-full left-0 mb-2': position === 'top-left',
}
);

🔹dropdownClass➜ Dynamically computes the CSS classes for positioning the dropdown menu based on the. position prop.

9. Dropdown Toggle Button Creation:

<button
id={id}
aria-label='Toggle dropdown'
aria-haspopup='true'
aria-expanded={isOpen}
type='button'
onClick={() => setIsOpen(!isOpen)}
className={classNames(
'flex justify-between items-center gap-5 rounded w-full py-2 px-4 bg-blue-500 text-white',
style
)}
>
<span>{selectedItem?.name || title}</span>
<GoChevronDown
size={20}
className={classNames('transform duration-500 ease-in-out', {
'rotate-180': isOpen,
})}
/>
</button>

🔹A button element is created with the title or selectedItem text and a chevron icon.

🔹The button toggles the isOpen state when clicked.

10. Dropdown Content Creation:

{isOpen && (
<div aria-label='Dropdown menu' className={dropdownClass}>
<ul
role='menu'
aria-labelledby={id}
aria-orientation='vertical'
className='leading-10'
>
{data?.map((item) => (
<li
key={item.id}
onClick={() => handleChange(item)}
className={classNames(
'flex items-center cursor-pointer hover:bg-gray-200 px-3',
{ 'bg-gray-300': selectedItem?.id === item.id }
)}
>
{hasImage && (
<img
src={item.imageUrl}
alt='image'
loading='lazy'
className='w-8 h-8 rounded-full bg-gray-400 object-cover me-2'
/>
)}
<span>{item.name}</span>
</li>
))}
</ul>
</div>
)}

🔹If the dropdown is open (isOpen is true), a div representing the dropdown menu is rendered.

🔹The dropdown menu contains a list of items generated from the data prop.

🔹Each item in the list is represented by an <li> element.

🔹If hasImage is true, an image is displayed next to each item.

Usage: ⤵️

const handleSelect = (id: string) => {
console.log(`Selected item with id ${id}`);
};

<Dropdown
id='person'
title='Select Person'
data={data}
hasImage
style='bg-purple-800'
selectedId='3'
onSelect={handleSelect}
/>

✔️ Here is Full Code: 👇

Dropdown.tsx

import { useEffect, useRef, useState } from 'react';
import classNames from 'classnames';
import { GoChevronDown } from 'react-icons/go';
import useOutsideClick from '../hooks/useOutsideClick';

interface DropdownItem {
id: string;
name: string;
imageUrl?: string;
}

interface DropdownProps {
id: string;
title?: string;
data: DropdownItem[];
position?: 'bottom-right' | 'bottom-left' | 'top-right' | 'top-left';
hasImage?: boolean;
style?: string;
selectedId?: string;
onSelect?: (id: string) => void;
}

const Dropdown = ({
id,
title = 'Select',
data,
position = 'bottom-left',
hasImage,
style,
selectedId,
onSelect,
}: DropdownProps) => {
const [isOpen, setIsOpen] = useState<boolean>(false);
const [selectedItem, setSelectedItem] = useState<DropdownItem | undefined>(
selectedId ? data?.find((item) => item.id === selectedId) : undefined
);

const handleChange = (item: DropdownItem) => {
setSelectedItem(item);
onSelect && onSelect(item.id);
setIsOpen(false);
};

useEffect(() => {
if (selectedId && data) {
const newSelectedItem = data.find((item) => item.id === selectedId);
newSelectedItem && setSelectedItem(newSelectedItem);
} else {
setSelectedItem(undefined);
}
}, [selectedId, data]);

const dropdownRef = useRef<HTMLDivElement>(null);
useOutsideClick({
ref: dropdownRef,
handler: () => setIsOpen(false),
});

const dropdownClass = classNames(
'absolute bg-gray-100 w-max max-h-52 overflow-y-auto py-3 rounded shadow-md z-10',
{
'top-full right-0 mt-2': position === 'bottom-right',
'top-full left-0 mt-2': position === 'bottom-left',
'bottom-full right-0 mb-2': position === 'top-right',
'bottom-full left-0 mb-2': position === 'top-left',
}
);

return (
<div ref={dropdownRef} className='relative'>
<button
id={id}
aria-label='Toggle dropdown'
aria-haspopup='true'
aria-expanded={isOpen}
type='button'
onClick={() => setIsOpen(!isOpen)}
className={classNames(
'flex justify-between items-center gap-5 rounded w-full py-2 px-4 bg-blue-500 text-white',
style
)}
>
<span>{selectedItem?.name || title}</span>
<GoChevronDown
size={20}
className={classNames('transform duration-500 ease-in-out', {
'rotate-180': isOpen,
})}
/>
</button>
{/* Open */}
{isOpen && (
<div aria-label='Dropdown menu' className={dropdownClass}>
<ul
role='menu'
aria-labelledby={id}
aria-orientation='vertical'
className='leading-10'
>
{data?.map((item) => (
<li
key={item.id}
onClick={() => handleChange(item)}
className={classNames(
'flex items-center cursor-pointer hover:bg-gray-200 px-3',
{ 'bg-gray-300': selectedItem?.id === item.id }
)}
>
{hasImage && (
<img
src={item.imageUrl}
alt='image'
loading='lazy'
className='w-8 h-8 rounded-full bg-gray-400 object-cover me-2'
/>
)}
<span>{item.name}</span>
</li>
))}
</ul>
</div>
)}
</div>
);
};

export default Dropdown;

useOutSideClick Hook Code: ⤵️

useOutsideClick.tsx

import { useEffect } from 'react';

interface OutsideClickHandlerProps {
ref: React.RefObject<HTMLElement>;
handler: () => void;
}

const useOutsideClick = ({ ref, handler }: OutsideClickHandlerProps) => {
useEffect(() => {
const handleClickOutside = (event: MouseEvent) => {
if (ref.current && !ref.current.contains(event.target as Node)) {
handler();
}
};

document.addEventListener('mousedown', handleClickOutside);

return () => {
document.removeEventListener('mousedown', handleClickOutside);
};
}, [ref, handler]);
};

export default useOutsideClick;

Sample Data (JSON): ⤵️

data.json

[
{
"id": "1",
"name": "Minnie Barrett",
"imageUrl":
"https://images.unsplash.com/photo-1534528741775-53994a69daeb?q=80&w=1964&auto=format&fit=crop&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D"
},
{
"id": "2",
"name": "Andy Holmes",
"imageUrl":
"https://images.unsplash.com/photo-1506794778202-cad84cf45f1d?q=80&w=1887&auto=format&fit=crop&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D"
},
{
"id": "3",
"name": "Felicia Watts",
"imageUrl":
"https://images.unsplash.com/photo-1544005313-94ddf0286df2?q=80&w=1888&auto=format&fit=crop&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D"
},
{
"id": "4",
"name": "Hailey Green",
"imageUrl":
"https://images.unsplash.com/photo-1494790108377-be9c29b29330?q=80&w=1887&auto=format&fit=crop&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D"
},
{
"id": "5",
"name": "Jeremiah Hughes",
"imageUrl":
"https://images.unsplash.com/photo-1500648767791-00dcc994a43e?q=80&w=1887&auto=format&fit=crop&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D"
},
{
"id": "6",
"name": "Amy Perkins",
"imageUrl":
"https://images.unsplash.com/photo-1587677171791-8b93c752999b?q=80&w=1949&auto=format&fit=crop&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D"
}
]

Github Link:

Live Demo:

Conclusion

In conclusion, a custom Dropdown component was created and explained for usage in React applications. The component can be utilized in different positions and styles, allowing users to select items from a list. Developed using TypeScript, Tailwind CSS, React Icons, and Classnames, this Dropdown component offers significant convenience in the user interface development process and can be widely used in React applications.

--

--

Kader Biral
Kader Biral

No responses yet