Menu

Material Design 3 dropdown menu component with items, dividers, icons, and various positioning options

Menu

Menus display a list of choices on temporary surfaces. @duskmoon-dev/core provides Material Design 3-inspired dropdown menus with support for items, dividers, icons, and various customization options.

Basic Usage

A basic menu requires a container with the menu class. Use menu-show to toggle visibility.

Basic Menu

Standard Items

Menu items can be buttons or links:

Standard Menu Items

Items with Icons

Add icons to menu items using the menu-item-icon class:

Menu Items with Icons

Items with Trailing Elements

Add keyboard shortcuts, badges, or other trailing elements:

Menu Items with Trailing Elements

Use dividers to separate related groups of menu items:

Menu with Dividers

Add section headers to organize menu items:

Menu with Labels

Active States

Highlight the currently selected menu item with active states:

Active Menu Item

Active Color Variants

Use different color schemes for active items:

Active Color Variants

Disabled Items

Disable menu items that are not currently available:

Disabled Menu Item

Checkboxes and Radio Buttons

Create selectable menu items:

Checkbox and Radio Menu Items

Position menus relative to their trigger element:

Menu Positioning

Create nested submenus for hierarchical navigation:

Menu with Submenu

Size Variants

Compact Menu

Smaller menu for dense interfaces:

Compact Menu

Wide Menu

Wider menu for longer text content:

Wide Menu

Dense Menu

Remove padding between items for a more compact appearance:

Dense Menu

Surface Variants

Use alternative surface colors:

Menu Surface Variant

Context Menu

Create right-click context menus using fixed positioning:

Context Menu

Interactive Example

Here’s a complete example with JavaScript to toggle menu visibility:

Interactive Menu Example

Best Practices

Accessibility

  • Use semantic <ul> and <li> elements for proper structure
  • Ensure menu items are keyboard navigable
  • Add aria-label or aria-labelledby to menus
  • Use role="menu" and role="menuitem" when appropriate
  • Support ESC key to close menus

Accessible Menu

Visual Hierarchy

  • Use dividers to group related items
  • Use labels for section headers
  • Limit menu depth (avoid deeply nested submenus)
  • Place destructive actions (like “Delete”) at the bottom

Content Guidelines

  • Use clear, concise labels
  • Lead with strong verbs (e.g., “Create”, “Edit”, “Delete”)
  • Group similar actions together
  • Show keyboard shortcuts for power users

Touch Targets

Ensure menu items are large enough for touch interfaces (minimum 44×44px). The default menu-item padding meets this requirement.

Framework Examples

React

import { useState, useRef, useEffect } from 'react';

interface MenuProps {
  trigger: React.ReactNode;
  items: Array<{
    label: string;
    icon?: React.ReactNode;
    onClick?: () => void;
    disabled?: boolean;
    divider?: boolean;
  }>;
}

export function Menu({ trigger, items }: MenuProps) {
  const [isOpen, setIsOpen] = useState(false);
  const menuRef = useRef<HTMLDivElement>(null);

  useEffect(() => {
    const handleClickOutside = (e: MouseEvent) => {
      if (menuRef.current && !menuRef.current.contains(e.target as Node)) {
        setIsOpen(false);
      }
    };

    document.addEventListener('click', handleClickOutside);
    return () => document.removeEventListener('click', handleClickOutside);
  }, []);

  return (
    <div ref={menuRef} style={{ position: 'relative', display: 'inline-block' }}>
      <div onClick={() => setIsOpen(!isOpen)}>{trigger}</div>

      <ul className={`menu menu-bottom ${isOpen ? 'menu-show' : ''}`}>
        {items.map((item, index) => (
          item.divider ? (
            <li key={index}><hr className="menu-divider" /></li>
          ) : (
            <li key={index}>
              <button
                className={`menu-item ${item.disabled ? 'menu-item-disabled' : ''}`}
                onClick={() => {
                  item.onClick?.();
                  setIsOpen(false);
                }}
                disabled={item.disabled}
              >
                {item.icon && <span className="menu-item-icon">{item.icon}</span>}
                {item.label}
              </button>
            </li>
          )
        ))}
      </ul>
    </div>
  );
}

// Usage
<Menu
  trigger={<button className="btn btn-primary">Options</button>}
  items={[
    { label: 'Edit', onClick: () => console.log('Edit') },
    { label: 'Duplicate', onClick: () => console.log('Duplicate') },
    { divider: true },
    { label: 'Delete', onClick: () => console.log('Delete') },
  ]}
/>

Vue

<template>
  <div ref="menuContainer" class="inline-block relative">
    <div @click="toggle">
      <slot name="trigger" />
    </div>

    <ul :class="['menu', 'menu-bottom', { 'menu-show': isOpen }]">
      <li v-for="(item, index) in items" :key="index">
        <hr v-if="item.divider" class="menu-divider" />
        <button
          v-else
          :class="['menu-item', { 'menu-item-disabled': item.disabled }]"
          :disabled="item.disabled"
          @click="handleClick(item)"
        >
          <span v-if="item.icon" class="menu-item-icon" v-html="item.icon" />
          {{ item.label }}
        </button>
      </li>
    </ul>
  </div>
</template>

<script setup>
import { ref, onMounted, onUnmounted } from 'vue';

const props = defineProps({
  items: {
    type: Array,
    required: true
  }
});

const isOpen = ref(false);
const menuContainer = ref(null);

const toggle = () => {
  isOpen.value = !isOpen.value;
};

const handleClick = (item) => {
  if (item.onClick) {
    item.onClick();
  }
  isOpen.value = false;
};

const handleClickOutside = (e) => {
  if (menuContainer.value && !menuContainer.value.contains(e.target)) {
    isOpen.value = false;
  }
};

onMounted(() => {
  document.addEventListener('click', handleClickOutside);
});

onUnmounted(() => {
  document.removeEventListener('click', handleClickOutside);
});
</script>

<!-- Usage -->
<Menu :items="menuItems">
  <template #trigger>
    <button class="btn btn-primary">Options</button>
  </template>
</Menu>

API Reference

Class Names

ClassDescription
.menuBase menu container (required)
.menu-showShows the menu (toggle visibility)
.menu-itemMenu item (button or link)
.menu-item-iconIcon within a menu item
.menu-item-trailingTrailing element (shortcut, badge)
.menu-item-activeActive menu item (primary variant)
.menu-item-active-primaryActive menu item with primary color
.menu-item-active-secondaryActive menu item with secondary color
.menu-item-active-tertiaryActive menu item with tertiary color
.menu-item-disabledDisabled menu item
.menu-item-submenuMenu item with submenu indicator
.menu-item-checkboxCheckbox menu item
.menu-item-radioRadio button menu item
.checkedChecked state for checkbox/radio items
.menu-dividerHorizontal divider between items
.menu-labelSection label/header
.menu-submenuNested submenu container
.menu-topPosition menu above trigger
.menu-bottomPosition menu below trigger
.menu-leftPosition menu to the left
.menu-rightPosition menu to the right
.menu-compactSmaller, more compact menu
.menu-wideWider menu for longer content
.menu-denseRemove padding between items
.menu-contextFixed positioning for context menus
.menu-surface-container-highestAlternative surface color

Combinations

Combine classes for different effects:

Combined Menu Classes

See Also