Skip to main content

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​

PropTypeDefaultDescription
isOpenboolean-Whether the dropdown is currently open
onClosefunction-Callback function when dropdown should close
childrenReactNode-The content to display in the dropdown
classNamestring''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>

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' },
]
<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>
<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>
<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​

  1. Always provide an anchorRef for proper positioning
  2. Use meaningful labels for trigger buttons
  3. Include close functionality in dropdown items
  4. Consider mobile users with touch-friendly targets
  5. Use appropriate z-index to avoid layering issues
  6. 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
  • Button - Used as trigger elements for dropdowns
  • NavLink - For navigation links within dropdowns
  • NavIcon - For icon-based triggers and actions
  • Card - Often used as a container for dropdown examples