Gecko UIGecko UI

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

Root container component that manages menu state.

<Menu>{/* MenuButton and MenuItems */}</Menu>

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>

Container for menu options. Handles positioning and animations.

PropTypeDefaultDescription
anchorPlacement-Menu position relative to button
asElementType'div'Element type to render as
classNamestring | Function-CSS class or render function

Individual selectable option within the menu.

PropTypeDescription
asElementTypeElement type (default: 'button')
disabledbooleanDisable the menu item
onClick() => voidClick handler
childrenReactNode | FunctionContent or render function

Render function receives: { focus: boolean, disabled: boolean }

Visual divider between menu groups or items.

<MenuSeparator className="my-1 border-t" />

Groups related menu items together under a heading.

<MenuSection>
  <MenuHeading>Section Title</MenuHeading>
  <MenuItem>Item 1</MenuItem>
  <MenuItem>Item 2</MenuItem>
</MenuSection>

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 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 button
  • GeckoUIMenu__items - Menu items container
  • GeckoUIMenu__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
  • Dropdown - Simplified dropdown for common use cases
  • Select - Form select input
  • Button - Standalone button component