Menu
Styled wrapper around HeadlessUI Menu for accessible dropdown menus
Menu
A styled wrapper around HeadlessUI's Menu component for creating accessible, keyboard-navigable dropdown menus and action lists. Provides pre-styled components with full HeadlessUI functionality for building complex menu patterns.
Installation
import {
Menu,
MenuButton,
MenuItem,
MenuItems,
MenuSeparator,
MenuSection,
MenuHeading
} from '@geckoui/geckoui';Basic Usage
import { Menu, MenuButton, MenuItem, MenuItems } from '@geckoui/geckoui';
function Example() {
return (
<Menu>
<MenuButton as="button">Options</MenuButton>
<MenuItems anchor="bottom start" className="w-48">
<MenuItem onClick={() => console.log('Edit')}>
Edit
</MenuItem>
<MenuItem onClick={() => console.log('Duplicate')}>
Duplicate
</MenuItem>
<MenuItem onClick={() => console.log('Delete')}>
Delete
</MenuItem>
</MenuItems>
</Menu>
);
}Components
Menu
Root container component that manages menu state.
<Menu>{/* MenuButton and MenuItems */}</Menu>MenuButton
Trigger element that opens the menu. Supports polymorphic rendering via the as prop.
<MenuButton as="button">Click me</MenuButton>
<MenuButton as={CustomButton}>Custom Button</MenuButton>MenuItems
Container for menu options. Handles positioning and animations.
| Prop | Type | Default | Description |
|---|---|---|---|
anchor | Placement | - | Menu position relative to button |
as | ElementType | 'div' | Element type to render as |
className | string | Function | - | CSS class or render function |
MenuItem
Individual selectable option within the menu.
| Prop | Type | Description |
|---|---|---|
as | ElementType | Element type (default: 'button') |
disabled | boolean | Disable the menu item |
onClick | () => void | Click handler |
children | ReactNode | Function | Content or render function |
Render function receives: { focus: boolean, disabled: boolean }
MenuSeparator
Visual divider between menu groups or items.
<MenuSeparator className="my-1 border-t" />MenuSection
Groups related menu items together under a heading.
<MenuSection>
<MenuHeading>Section Title</MenuHeading>
<MenuItem>Item 1</MenuItem>
<MenuItem>Item 2</MenuItem>
</MenuSection>MenuHeading
Non-interactive header within menu sections.
<MenuHeading className="px-3 py-2 text-xs font-semibold">
Section Title
</MenuHeading>Examples
With Icons
import { Edit, Copy, Trash } from 'lucide-react';
<Menu>
<MenuButton as="button">Actions</MenuButton>
<MenuItems anchor="bottom start" className="w-48">
<MenuItem onClick={() => console.log('Edit')}>
<div className="flex items-center gap-3">
<Edit className="size-4" />
<span>Edit</span>
</div>
</MenuItem>
<MenuItem onClick={() => console.log('Delete')}>
<div className="flex items-center gap-3">
<Trash className="size-4 text-red-600" />
<span className="text-red-600">Delete</span>
</div>
</MenuItem>
</MenuItems>
</Menu>With Sections and Separators
<Menu>
<MenuButton as="button">More Actions</MenuButton>
<MenuItems anchor="bottom start" className="w-56">
<MenuSection>
<MenuHeading className="text-xs font-semibold text-gray-500 uppercase">
File Actions
</MenuHeading>
<MenuItem onClick={() => console.log('Download')}>
<div className="flex items-center gap-3">
<Download className="size-4" />
<span>Download</span>
</div>
</MenuItem>
<MenuItem onClick={() => console.log('Share')}>
<div className="flex items-center gap-3">
<Share className="size-4" />
<span>Share</span>
</div>
</MenuItem>
</MenuSection>
<MenuSeparator className="my-2 border-t" />
<MenuSection>
<MenuHeading className="text-xs font-semibold text-gray-500 uppercase">
Manage
</MenuHeading>
<MenuItem onClick={() => console.log('Settings')}>
Settings
</MenuItem>
<MenuItem onClick={() => console.log('Delete')}>
Delete
</MenuItem>
</MenuSection>
</MenuItems>
</Menu>Disabled Items
<Menu>
<MenuButton as="button">Actions</MenuButton>
<MenuItems anchor="bottom start" className="w-48">
<MenuItem onClick={() => console.log('Available')}>
Available Action
</MenuItem>
<MenuItem disabled>
{({ disabled }) => (
<span className={disabled ? 'opacity-50' : ''}>
Disabled Action
</span>
)}
</MenuItem>
<MenuItem onClick={() => console.log('Another')}>
Another Action
</MenuItem>
</MenuItems>
</Menu>Custom Button Component
import { Button } from '@geckoui/geckoui';
<Menu>
<MenuButton as={Button} variant="filled">
User Menu
</MenuButton>
<MenuItems anchor="bottom end" className="w-48 mt-2">
<MenuItem onClick={() => console.log('Profile')}>Profile</MenuItem>
<MenuItem onClick={() => console.log('Settings')}>Settings</MenuItem>
<MenuSeparator className="my-1 border-t" />
<MenuItem onClick={() => console.log('Logout')}>
<span className="text-red-600">Logout</span>
</MenuItem>
</MenuItems>
</Menu>Anchor Positioning
// Opens upward
<Menu>
<MenuButton as="button">Top Start</MenuButton>
<MenuItems anchor="top start" className="w-40 mb-2">
<MenuItem>Option 1</MenuItem>
<MenuItem>Option 2</MenuItem>
</MenuItems>
</Menu>
// Aligns to right edge
<Menu>
<MenuButton as="button">Bottom End</MenuButton>
<MenuItems anchor="bottom end" className="w-40 mt-2">
<MenuItem>Option 1</MenuItem>
<MenuItem>Option 2</MenuItem>
</MenuItems>
</Menu>Render Functions
Both MenuItem and class names can use render functions to access state:
MenuItem Render Function
<MenuItem onClick={handleClick}>
{({ focus, disabled }) => (
<span className={`
${focus ? 'font-medium' : ''}
${disabled ? 'opacity-50' : ''}
`}>
{focus ? '→ ' : ''}Menu Item
</span>
)}
</MenuItem>ClassName Render Function
<MenuItem
className={({ focus, disabled }) => `
${focus ? 'font-medium' : ''}
${disabled ? 'opacity-50 cursor-not-allowed' : ''}
`}
>
Menu Item
</MenuItem>Keyboard Navigation
- Click/Space/Enter: Toggle menu
- Arrow Up/Down: Navigate items
- Home/End: Jump to first/last item
- Escape: Close menu
- Tab: Close menu and move focus
- Type: Search items by typing
Anchor Positions
Available anchor positions:
"bottom start"- Below button, aligned to left"bottom"- Below button, centered"bottom end"- Below button, aligned to right"top start"- Above button, aligned to left"top"- Above button, centered"top end"- Above button, aligned to right
Styling
The component uses BEM-style class names:
GeckoUIMenu__button- Menu buttonGeckoUIMenu__items- Menu items containerGeckoUIMenu__item- Individual menu item
Additional styling should be applied via the className prop.
HeadlessUI Documentation
This component wraps HeadlessUI's Menu. For complete API documentation, see: HeadlessUI Menu Documentation
Accessibility
- Fully keyboard navigable
- ARIA attributes automatically applied
- Focus management
- Screen reader support
- Automatic ID generation
- Type-to-search functionality