Dropdown
The Dropdown component is a flexible overlay component that displays content in a floating menu. It provides click-outside-to-close functionality and can be positioned relative to any anchor element, making it perfect for navigation menus, action menus, and contextual options.
Overview
The Dropdown component is designed to provide flexible overlay menus with:
- Click-outside-to-close functionality
- Anchor-based positioning
- Customizable content and styling
- Multiple trigger types (buttons, icons, etc.)
- Various sizes and color themes
- Responsive design with proper z-index management
Basic Usage
import { Dropdown, Button } from '@/components/atoms'
import { useRef, useState } from 'react'
function MyComponent() {
const [isOpen, setIsOpen] = useState(false)
const anchorRef = useRef(null)
return (
<div className='relative inline-block'>
<Button ref={anchorRef} onClick={() => setIsOpen(!isOpen)} className='btn-primary'>
Open Dropdown
</Button>
<Dropdown isOpen={isOpen} onClose={() => setIsOpen(false)} anchorRef={{ current: anchorRef.current }}>
<div className='bg-white rounded shadow p-2 min-w-[8rem] flex flex-col gap-1'>
<Button className='w-full text-left px-4 py-2 hover:bg-gray-100 rounded'>
Item 1
</Button>
<Button className='w-full text-left px-4 py-2 hover:bg-gray-100 rounded'>
Item 2
</Button>
<Button className='w-full text-left px-4 py-2 hover:bg-gray-100 rounded'>
Item 3
</Button>
</div>
</Dropdown>
</div>
)
}
Props
| Prop | Type | Default | Description |
|---|---|---|---|
isOpen | boolean | - | Whether the dropdown is currently open |
onClose | function | - | Callback function when dropdown should close |
children | ReactNode | - | The content to display in the dropdown |
className | string | '' | Additional CSS classes for the dropdown |
anchorRef | { current: HTMLElement } | - | Reference to the anchor element for positioning |
Basic Dropdown
Simple Dropdown
<Card>
<h4 className='mb-2'>Basic Dropdown</h4>
<div className='relative inline-block'>
<Button ref={(el) => (refs.current.basic = el)} onClick={() => toggle('basic')} className='btn-primary'>
Open Dropdown
</Button>
<Dropdown isOpen={!!open.basic} onClose={() => close('basic')} anchorRef={{ current: refs.current.basic }}>
<div className='bg-white rounded shadow p-2 min-w-[8rem] flex flex-col gap-1'>
<Button className='w-full text-left px-4 py-2 hover:bg-gray-100 rounded' onClick={() => close('basic')}>
Item 1
</Button>
<Button className='w-full text-left px-4 py-2 hover:bg-gray-100 rounded' onClick={() => close('basic')}>
Item 2
</Button>
<Button className='w-full text-left px-4 py-2 hover:bg-gray-100 rounded' onClick={() => close('basic')}>
Item 3
</Button>
</div>
</Dropdown>
</div>
</Card>
Dropdown Sizes
Different Width Options
<Card>
<h4 className='mb-2'>Dropdown Sizes</h4>
<div className='flex flex-wrap gap-4'>
{sizes.map((sz, idx) => (
<div key={sz.label} className='relative inline-block'>
<Button ref={(el) => (refs.current[`size${idx}`] = el)} onClick={() => toggle(`size${idx}`)} className='btn-primary'>
{sz.label}
</Button>
<Dropdown isOpen={!!open[`size${idx}`]} onClose={() => close(`size${idx}`)} anchorRef={{ current: refs.current[`size${idx}`] }}>
<div className={`bg-white rounded shadow p-2 ${sz.className} flex flex-col gap-1`}>
<Button className='w-full text-left px-4 py-2 hover:bg-gray-100 rounded' onClick={() => close(`size${idx}`)}>
Size: {sz.label}
</Button>
</div>
</Dropdown>
</div>
))}
</div>
</Card>
Size Configuration
const sizes = [
{ label: 'Small', className: 'w-32' },
{ label: 'Medium', className: 'w-48' },
{ label: 'Large', className: 'w-72' },
]
Dropdown with Links
Navigation Links
<Card>
<h4 className='mb-2'>Dropdown with Links</h4>
<div className='relative inline-block'>
<Button ref={(el) => (refs.current.link = el)} onClick={() => toggle('link')} className='btn-transparent'>
Dropdown Links
</Button>
<Dropdown isOpen={!!open.link} onClose={() => close('link')} anchorRef={{ current: refs.current.link }}>
<div className='bg-white rounded shadow p-2 min-w-[8rem] flex flex-col gap-1'>
<NavLink href='#' className='w-full text-left px-4 py-2 hover:bg-gray-100 rounded' onClick={() => close('link')}>
Profile
</NavLink>
<NavLink href='#' className='w-full text-left px-4 py-2 hover:bg-gray-100 rounded' onClick={() => close('link')}>
Settings
</NavLink>
<NavLink href='#' className='w-full text-left px-4 py-2 hover:bg-gray-100 rounded text-red-500' onClick={() => close('link')}>
Logout
</NavLink>
</div>
</Dropdown>
</div>
</Card>
Custom Triggers
Icon Trigger (Ellipsis)
<Card>
<h4 className='mb-2'>Custom Trigger (Ellipsis)</h4>
<div className='relative inline-block'>
<Button ref={(el) => (refs.current.ellipsis = el)} onClick={() => toggle('ellipsis')} className='p-2' aria-label='Open actions'>
<NavIcon icon='lucide:ellipsis' size={20} />
</Button>
<Dropdown isOpen={!!open.ellipsis} onClose={() => close('ellipsis')} anchorRef={{ current: refs.current.ellipsis }}>
<div className='bg-white rounded shadow p-2 min-w-[8rem] flex flex-col gap-1'>
<Button className='w-full text-left px-4 py-2 hover:bg-gray-100 rounded' onClick={() => close('ellipsis')}>
Action 1
</Button>
<Button className='w-full text-left px-4 py-2 hover:bg-gray-100 rounded' onClick={() => close('ellipsis')}>
Action 2
</Button>
</div>
</Dropdown>
</div>
</Card>
Styled Dropdowns
Outlined Dropdown
<Card>
<h4 className='mb-2'>Outlined Dropdown</h4>
<div className='relative inline-block'>
<Button ref={(el) => (refs.current.outlined = el)} onClick={() => toggle('outlined')} className='btn-primary outlined'>
Open Dropdown
</Button>
<Dropdown isOpen={!!open.outlined} onClose={() => close('outlined')} anchorRef={{ current: refs.current.basic }}>
<div className='bg-white rounded shadow p-2 min-w-[8rem] flex flex-col gap-1'>
<Button className='w-full text-left px-4 py-2 hover:bg-gray-100 rounded' onClick={() => close('basic')}>
Item 1
</Button>
<Button className='w-full text-left px-4 py-2 hover:bg-gray-100 rounded' onClick={() => close('basic')}>
Item 2
</Button>
<Button className='w-full text-left px-4 py-2 hover:bg-gray-100 rounded' onClick={() => close('basic')}>
Item 3
</Button>
</div>
</Dropdown>
</div>
</Card>
Colored Dropdowns
Blue Dropdown
<div className='relative inline-block'>
<Button ref={(el) => (refs.current.coloredBlue = el)} onClick={() => toggle('coloredBlue')} className='btn-primary rounded'>
Blue
</Button>
<Dropdown isOpen={!!open.coloredBlue} onClose={() => close('coloredBlue')} anchorRef={{ current: refs.current.coloredBlue }}>
<div className='bg-blue-500 text-white border border-blue-600 rounded shadow p-2 min-w-[8rem] flex flex-col gap-1'>
<Button className='w-full text-left px-4 py-2 hover:bg-blue-600 rounded bg-transparent text-white' onClick={() => close('coloredBlue')}>
Blue Action
</Button>
</div>
</Dropdown>
</div>
Green Dropdown
<div className='relative inline-block'>
<Button ref={(el) => (refs.current.coloredGreen = el)} onClick={() => toggle('coloredGreen')} className='btn-success rounded'>
Green
</Button>
<Dropdown isOpen={!!open.coloredGreen} onClose={() => close('coloredGreen')} anchorRef={{ current: refs.current.coloredGreen }}>
<div className='bg-green-500 text-white border border-green-600 rounded shadow p-2 min-w-[8rem] flex flex-col gap-1'>
<Button className='w-full text-left px-4 py-2 hover:bg-green-600 rounded bg-transparent text-white' onClick={() => close('coloredGreen')}>
Green Action
</Button>
</div>
</Dropdown>
</div>
Yellow Dropdown
<div className='relative inline-block'>
<Button ref={(el) => (refs.current.coloredYellow = el)} onClick={() => toggle('coloredYellow')} className='btn-warning rounded'>
Yellow
</Button>
<Dropdown isOpen={!!open.coloredYellow} onClose={() => close('coloredYellow')} anchorRef={{ current: refs.current.coloredYellow }}>
<div className='bg-yellow-400 text-gray-900 border border-yellow-500 rounded shadow p-2 min-w-[8rem] flex flex-col gap-1'>
<Button className='w-full text-left px-4 py-2 hover:bg-yellow-500 hover:text-white rounded bg-transparent' onClick={() => close('coloredYellow')}>
Yellow Action
</Button>
</div>
</Dropdown>
</div>
Red Dropdown
<div className='relative inline-block'>
<Button ref={(el) => (refs.current.coloredRed = el)} onClick={() => toggle('coloredRed')} className='btn-danger rounded'>
Red
</Button>
<Dropdown isOpen={!!open.coloredRed} onClose={() => close('coloredRed')} anchorRef={{ current: refs.current.coloredRed }}>
<div className='bg-red-500 text-white border border-red-600 rounded shadow p-2 min-w-[8rem] flex flex-col gap-1'>
<Button className='w-full text-left px-4 py-2 hover:bg-red-600 rounded bg-transparent text-white' onClick={() => close('coloredRed')}>
Red Action
</Button>
</div>
</Dropdown>
</div>
State Management
Using useState and useRef
import { useRef, useState } from 'react'
const DropdownExample = () => {
const [open, setOpen] = useState({})
const refs = useRef({})
const toggle = (key) => setOpen((prev) => ({ ...prev, [key]: !prev[key] }))
const close = (key) => setOpen((prev) => ({ ...prev, [key]: false }))
return (
<div className='relative inline-block'>
<Button ref={(el) => (refs.current.example = el)} onClick={() => toggle('example')} className='btn-primary'>
Toggle Dropdown
</Button>
<Dropdown isOpen={!!open.example} onClose={() => close('example')} anchorRef={{ current: refs.current.example }}>
<div className='bg-white rounded shadow p-2 min-w-[8rem]'>
<Button className='w-full text-left px-4 py-2 hover:bg-gray-100 rounded' onClick={() => close('example')}>
Action Item
</Button>
</div>
</Dropdown>
</div>
)
}
Examples
Basic Dropdown
<Card>
<h4 className='mb-2'>Basic Dropdown</h4>
<div className='relative inline-block'>
<Button ref={(el) => (refs.current.basic = el)} onClick={() => toggle('basic')} className='btn-primary'>
Open Dropdown
</Button>
<Dropdown isOpen={!!open.basic} onClose={() => close('basic')} anchorRef={{ current: refs.current.basic }}>
<div className='bg-white rounded shadow p-2 min-w-[8rem] flex flex-col gap-1'>
<Button className='w-full text-left px-4 py-2 hover:bg-gray-100 rounded' onClick={() => close('basic')}>
Item 1
</Button>
<Button className='w-full text-left px-4 py-2 hover:bg-gray-100 rounded' onClick={() => close('basic')}>
Item 2
</Button>
<Button className='w-full text-left px-4 py-2 hover:bg-gray-100 rounded' onClick={() => close('basic')}>
Item 3
</Button>
</div>
</Dropdown>
</div>
</Card>
Dropdown Sizes
<Card>
<h4 className='mb-2'>Dropdown Sizes</h4>
<div className='flex flex-wrap gap-4'>
{sizes.map((sz, idx) => (
<div key={sz.label} className='relative inline-block'>
<Button ref={(el) => (refs.current[`size${idx}`] = el)} onClick={() => toggle(`size${idx}`)} className='btn-primary'>
{sz.label}
</Button>
<Dropdown isOpen={!!open[`size${idx}`]} onClose={() => close(`size${idx}`)} anchorRef={{ current: refs.current[`size${idx}`] }}>
<div className={`bg-white rounded shadow p-2 ${sz.className} flex flex-col gap-1`}>
<Button className='w-full text-left px-4 py-2 hover:bg-gray-100 rounded' onClick={() => close(`size${idx}`)}>
Size: {sz.label}
</Button>
</div>
</Dropdown>
</div>
))}
</div>
</Card>
Dropdown with Links
<Card>
<h4 className='mb-2'>Dropdown with Links</h4>
<div className='relative inline-block'>
<Button ref={(el) => (refs.current.link = el)} onClick={() => toggle('link')} className='btn-transparent'>
Dropdown Links
</Button>
<Dropdown isOpen={!!open.link} onClose={() => close('link')} anchorRef={{ current: refs.current.link }}>
<div className='bg-white rounded shadow p-2 min-w-[8rem] flex flex-col gap-1'>
<NavLink href='#' className='w-full text-left px-4 py-2 hover:bg-gray-100 rounded' onClick={() => close('link')}>
Profile
</NavLink>
<NavLink href='#' className='w-full text-left px-4 py-2 hover:bg-gray-100 rounded' onClick={() => close('link')}>
Settings
</NavLink>
<NavLink href='#' className='w-full text-left px-4 py-2 hover:bg-gray-100 rounded text-red-500' onClick={() => close('link')}>
Logout
</NavLink>
</div>
</Dropdown>
</div>
</Card>
Custom Trigger (Ellipsis)
<Card>
<h4 className='mb-2'>Custom Trigger (Ellipsis)</h4>
<div className='relative inline-block'>
<Button ref={(el) => (refs.current.ellipsis = el)} onClick={() => toggle('ellipsis')} className='p-2' aria-label='Open actions'>
<NavIcon icon='lucide:ellipsis' size={20} />
</Button>
<Dropdown isOpen={!!open.ellipsis} onClose={() => close('ellipsis')} anchorRef={{ current: refs.current.ellipsis }}>
<div className='bg-white rounded shadow p-2 min-w-[8rem] flex flex-col gap-1'>
<Button className='w-full text-left px-4 py-2 hover:bg-gray-100 rounded' onClick={() => close('ellipsis')}>
Action 1
</Button>
<Button className='w-full text-left px-4 py-2 hover:bg-gray-100 rounded' onClick={() => close('ellipsis')}>
Action 2
</Button>
</div>
</Dropdown>
</div>
</Card>
Outlined Dropdown
<Card>
<h4 className='mb-2'>Outlined Dropdown</h4>
<div className='relative inline-block'>
<Button ref={(el) => (refs.current.outlined = el)} onClick={() => toggle('outlined')} className='btn-primary outlined'>
Open Dropdown
</Button>
<Dropdown isOpen={!!open.outlined} onClose={() => close('outlined')} anchorRef={{ current: refs.current.basic }}>
<div className='bg-white rounded shadow p-2 min-w-[8rem] flex flex-col gap-1'>
<Button className='w-full text-left px-4 py-2 hover:bg-gray-100 rounded' onClick={() => close('basic')}>
Item 1
</Button>
<Button className='w-full text-left px-4 py-2 hover:bg-gray-100 rounded' onClick={() => close('basic')}>
Item 2
</Button>
<Button className='w-full text-left px-4 py-2 hover:bg-gray-100 rounded' onClick={() => close('basic')}>
Item 3
</Button>
</div>
</Dropdown>
</div>
</Card>
Colored Dropdown
<Card>
<h4 className='mb-2'>Colored Dropdown</h4>
<div className='flex flex-wrap gap-4'>
{/* Blue Dropdown */}
<div className='relative inline-block'>
<Button ref={(el) => (refs.current.coloredBlue = el)} onClick={() => toggle('coloredBlue')} className='btn-primary rounded'>
Blue
</Button>
<Dropdown isOpen={!!open.coloredBlue} onClose={() => close('coloredBlue')} anchorRef={{ current: refs.current.coloredBlue }}>
<div className='bg-blue-500 text-white border border-blue-600 rounded shadow p-2 min-w-[8rem] flex flex-col gap-1'>
<Button className='w-full text-left px-4 py-2 hover:bg-blue-600 rounded bg-transparent text-white' onClick={() => close('coloredBlue')}>
Blue Action
</Button>
</div>
</Dropdown>
</div>
{/* Green Dropdown */}
<div className='relative inline-block'>
<Button ref={(el) => (refs.current.coloredGreen = el)} onClick={() => toggle('coloredGreen')} className='btn-success rounded'>
Green
</Button>
<Dropdown isOpen={!!open.coloredGreen} onClose={() => close('coloredGreen')} anchorRef={{ current: refs.current.coloredGreen }}>
<div className='bg-green-500 text-white border border-green-600 rounded shadow p-2 min-w-[8rem] flex flex-col gap-1'>
<Button className='w-full text-left px-4 py-2 hover:bg-green-600 rounded bg-transparent text-white' onClick={() => close('coloredGreen')}>
Green Action
</Button>
</div>
</Dropdown>
</div>
{/* Yellow Dropdown */}
<div className='relative inline-block'>
<Button ref={(el) => (refs.current.coloredYellow = el)} onClick={() => toggle('coloredYellow')} className='btn-warning rounded'>
Yellow
</Button>
<Dropdown isOpen={!!open.coloredYellow} onClose={() => close('coloredYellow')} anchorRef={{ current: refs.current.coloredYellow }}>
<div className='bg-yellow-400 text-gray-900 border border-yellow-500 rounded shadow p-2 min-w-[8rem] flex flex-col gap-1'>
<Button className='w-full text-left px-4 py-2 hover:bg-yellow-500 hover:text-white rounded bg-transparent' onClick={() => close('coloredYellow')}>
Yellow Action
</Button>
</div>
</Dropdown>
</div>
{/* Red Dropdown */}
<div className='relative inline-block'>
<Button ref={(el) => (refs.current.coloredRed = el)} onClick={() => toggle('coloredRed')} className='btn-danger rounded'>
Red
</Button>
<Dropdown isOpen={!!open.coloredRed} onClose={() => close('coloredRed')} anchorRef={{ current: refs.current.coloredRed }}>
<div className='bg-red-500 text-white border border-red-600 rounded shadow p-2 min-w-[8rem] flex flex-col gap-1'>
<Button className='w-full text-left px-4 py-2 hover:bg-red-600 rounded bg-transparent text-white' onClick={() => close('coloredRed')}>
Red Action
</Button>
</div>
</Dropdown>
</div>
</div>
</Card>
Styling
The Dropdown component uses Tailwind CSS classes and includes:
- Positioning: Absolute positioning with z-index management
- Responsive Design: Works on all screen sizes
- Custom Styling: Flexible content styling options
- Shadow Effects: Drop shadow for depth
- Border Radius: Rounded corners for modern appearance
- Hover States: Interactive hover effects
Accessibility
The Dropdown component includes accessibility features:
- Click Outside: Automatic closing when clicking outside
- Keyboard Navigation: Proper focus management
- Screen Reader Support: ARIA labels and semantic structure
- Focus Trapping: Prevents focus from escaping the dropdown
Best Practices
- Always provide an anchorRef for proper positioning
- Use meaningful labels for trigger buttons
- Include close functionality in dropdown items
- Consider mobile users with touch-friendly targets
- Use appropriate z-index to avoid layering issues
- Test keyboard navigation for accessibility compliance
Common Use Cases
- Navigation menus: User account options and settings
- Action menus: Contextual actions for items
- Filter dropdowns: Search and filter options
- Settings panels: Configuration options
- User menus: Profile, logout, and account actions
Related Components
Button- Used as trigger elements for dropdownsNavLink- For navigation links within dropdownsNavIcon- For icon-based triggers and actionsCard- Often used as a container for dropdown examples