Gecko UIGecko UI

Menu

Accessible dropdown menu for actions and navigation

Menu

An accessible, keyboard-navigable dropdown menu for actions and navigation. Built with Floating UI for positioning.

Installation

import { Menu, MenuItem, MenuTrigger } from '@geckoui/geckoui';

Basic Usage

<Menu label="Options">
  <MenuItem label="Edit" onClick={() => console.log('Edit')} />
  <MenuItem label="Duplicate" onClick={() => console.log('Duplicate')} />
  <MenuItem label="Delete" onClick={() => console.log('Delete')} />
</Menu>

Components

Root component that manages state, positioning, and renders the default button.

PropTypeDefaultDescription
labelstring"Menu"Text for the default button
disabledbooleanfalseDisable the menu
placementPlacement"bottom-start"Dropdown position
classNamestring-Wrapper class
menuClassNamestring-Dropdown panel class
buttonClassNamestring-Default button class

Individual action item within the menu. Closes the menu on click.

PropTypeDescription
labelstringItem text (rendered if no children)
onClick() => voidClick handler
disabledbooleanDisable the item
classNamestringAdditional class
childrenReactNode | FunctionCustom content or render function

Render function receives { focused: boolean }.

Custom trigger using render props. Replaces the default button.

<Menu>
  <MenuTrigger>
    {({ open, toggleMenu, disabled }) => (
      <button onClick={toggleMenu}>
        Actions {open ? '▲' : '▼'}
      </button>
    )}
  </MenuTrigger>
  <MenuItem label="Edit" onClick={() => {}} />
</Menu>

Examples

With Icons

import { Edit, Copy, Trash } from 'lucide-react';

<Menu>
  <MenuTrigger>
    {({ toggleMenu }) => (
      <Button onClick={toggleMenu}>
        Actions <ChevronDown className="size-4" />
      </Button>
    )}
  </MenuTrigger>
  <MenuItem label="Edit" onClick={() => console.log('Edit')}>
    <div className="flex items-center gap-3">
      <Edit className="size-4" />
      <span>Edit</span>
    </div>
  </MenuItem>
  <MenuItem label="Delete" 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>
</Menu>

Disabled Items

<Menu label="Actions">
  <MenuItem label="Available Action" onClick={() => console.log('Available')} />
  <MenuItem label="Disabled Action" disabled />
  <MenuItem label="Another Action" onClick={() => console.log('Another')} />
</Menu>

Custom Button

<Menu>
  <MenuTrigger>
    {({ toggleMenu }) => (
      <Button variant="filled" onClick={toggleMenu}>
        User Menu
      </Button>
    )}
  </MenuTrigger>
  <MenuItem label="Profile" onClick={() => console.log('Profile')} />
  <MenuItem label="Settings" onClick={() => console.log('Settings')} />
  <MenuItem label="Logout" onClick={() => console.log('Logout')}>
    <span className="text-red-600">Logout</span>
  </MenuItem>
</Menu>

Placement

<Menu label="Top Start" placement="top-start">
  <MenuItem label="Option 1" />
  <MenuItem label="Option 2" />
</Menu>

<Menu label="Bottom End" placement="bottom-end">
  <MenuItem label="Option 1" />
  <MenuItem label="Option 2" />
</Menu>

Render Function

Use a render function in children to access focus state:

<MenuItem label="Edit" onClick={handleEdit}>
  {({ focused }) => (
    <div className={focused ? 'font-bold' : ''}>
      {focused ? '→ ' : ''}Edit
    </div>
  )}
</MenuItem>

Keyboard Navigation

  • Enter/Space on button: Toggle menu
  • Arrow Down/Up: Navigate items
  • Enter/Space on item: Trigger action
  • Escape/Tab: Close menu

Styling

  • GeckoUIMenu - Wrapper
  • GeckoUIMenu__button - Default button
  • GeckoUIMenu__items - Dropdown panel
  • GeckoUIMenu__item - Individual item
    • data-focused - Present when focused
    • data-disabled - Present when disabled

Accessibility

  • role="menu" on dropdown panel
  • role="menuitem" on items
  • aria-haspopup and aria-expanded on button
  • Full keyboard navigation
  • Focus management
  • Select - Form select input
  • Button - Standalone button component