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