Components

Menu

Provide command menu interactions with roving focus, typeahead, and stable floating positioning for root and nested menus.

Release Scope: Canonical Implementation: Core Site Visibility: Listed Adapters: React, Vanilla, Solid, Vue, Svelte Category: navigation

1. Live Preview

vanilla live preview activates on the client; source HTML keeps a readable preview island and primitive contract.

vanilla menu preview activates on the client.

2. Installation

Install adapter-specific dependencies first, then complete setup via the installation guide. installation docs

pnpm add @nake-ui/primitives @nake-ui/adapter-vanilla @nake-ui/theme-vanilla

3. Usage

Minimal runnable usage for the current adapter.

import "@nake-ui/theme-vanilla/styles.css";
import { bindMenu } from "@nake-ui/adapter-vanilla";

const binding = bindMenu(nodes, items, options);
binding.destroy();

4. Examples

Hover

Use `openOnHover` for stable hover opening while sharing the same Floating positioner behavior as React.

Selected: None

import "@nake-ui/theme-vanilla/styles.css";
import { bindMenu } from "@nake-ui/adapter-vanilla";

if (typeof window !== "undefined" && typeof document !== "undefined") {
  const binding = bindMenu(
    {
      trigger,
      positioner,
      content,
      items: { open: itemOpen, share: itemShare, delete: itemDelete }
    },
    [
      { id: "open", textValue: "Open" },
      { id: "share", textValue: "Share" },
      { id: "delete", textValue: "Delete" }
    ],
    { id: "example-hover-menu", defaultOpen: false, openOnHover: true, matchAnchorWidth: false }
  );

  // cleanup on unmount
  binding.destroy();
}

trigger.textContent = "Hover me";

Nested

Compose parent and submenu bindings so nested actions open on the right with an arrow cue, aligned with the React demo.

Selected: None

import "@nake-ui/theme-vanilla/styles.css";
import { bindMenu } from "@nake-ui/adapter-vanilla";

if (typeof window !== "undefined" && typeof document !== "undefined") {
  const rootBinding = bindMenu(
    {
      root: rootMenu,
      trigger: rootTrigger,
      positioner: rootPositioner,
      content: rootContent,
      items: { rename: itemRename, duplicate: itemDuplicate }
    },
    [
      { id: "rename", textValue: "Rename" },
      { id: "duplicate", textValue: "Duplicate" }
    ],
    {
      id: "example-menu-root",
      defaultOpen: false,
      matchAnchorWidth: false
    }
  );

  const submenuBinding = bindMenu(
    {
      root: themeRoot,
      trigger: themeTrigger,
      positioner: themePositioner,
      content: themeContent,
      items: { 'theme-light': itemThemeLight, 'theme-dark': itemThemeDark }
    },
    [
      { id: "theme-light", textValue: "Light" },
      { id: "theme-dark", textValue: "Dark" }
    ],
    {
      id: "example-submenu",
      defaultOpen: false,
      openOnHover: true,
      placement: "right-start",
      offset: 8,
      matchAnchorWidth: false
    }
  );

  // cleanup on unmount
  rootBinding.destroy();
  submenuBinding.destroy();
}

5. RTL

The same structure running inside a `dir="rtl"` container.

vanilla menu preview activates on the client.

6. Closure Coverage

Complete

Closure Score: 100% (19/19)

Required Checks

  • schemaRegistered
  • specDocumented
  • reactMapper
  • siteExampleCoverage
  • siteA11yCoverage
  • siteVisible
  • schemaValidatorCoverage
  • schemaAriaCoverage
  • specValidatorRulesSection
  • specTestChecklistSection
  • specSsrHydrationSection
  • reactSsrHydrationCoverage
  • reactRenderLevelSsrCoverage
  • solidSsrCoverage
  • coreImplemented
  • primitiveUnitTest
  • domContractSnapshot
  • vanillaBinding
  • adapterParitySnapshot

All required closure checks are currently satisfied.

7. Purpose / Non-goals

Provide command menu interactions with roving focus, typeahead, and stable floating positioning for root and nested menus.

  • Rich styled menu presentation
  • Submenu visual animation system in core

8. Anatomy & Slot

  • root (optional structural wrapper in vanilla; always present in React composition)
  • anchor (optional positioning anchor)
  • trigger
  • positioner (optional floating layer wrapper)
  • content
  • item (repeatable)
  • separator (optional)
  • nested composition: submenu root + trigger + positioner + content

9. DOM Contract

<div data-ui="menu" data-slot="root" data-state="open">
  <button
    id="menu-trigger"
    data-ui="menu"
    data-slot="trigger"
    data-state="open"
    aria-haspopup="menu"
    aria-expanded="true"
    aria-controls="menu-content"
  ></button>
  <div
    data-ui="menu"
    data-slot="positioner"
    data-state="open"
    data-placement="bottom-start"
    data-side="bottom"
  >
    <div id="menu-content" data-slot="content" data-state="open" role="menu">
      <button
        data-slot="item"
        data-state="highlighted"
        data-highlighted="true"
        role="menuitem"
        tabindex="0"
      ></button>
      <button data-slot="item" data-state="idle" role="menuitem" tabindex="-1"></button>
      <div
        data-ui="menu"
        data-slot="separator"
        role="separator"
        aria-orientation="horizontal"
      ></div>
    </div>
  </div>
</div>

10. State & Events

State Model

  • open state: controlled (open + onOpenChange) and uncontrolled (defaultOpen)
  • highlighted item id driven by roving focus + typeahead
  • optional selection callback (onSelect / onItemSelect)
  • selecting an item closes the active menu tree, including all open submenu ancestors
  • hover mode state includes close-delay cancellation guards and transition boundary checks
  • positioner state reflection via data-placement and data-side when floating layer is rendered
  • first-open measurement settling in vanilla via deferred post-open sync pass

Event Model

  • trigger click toggles open state
  • ArrowUp / ArrowDown / Home / End move highlight
  • typeahead search by textValue
  • Enter / Space select highlighted item and close the current menu tree
  • item click selects the item and closes the current menu tree
  • item mouseenter / focus updates highlighted id
  • outside pointerdown dismiss for open layers while preserving in-tree menu boundaries
  • hover mode supports open-on-enter and delayed close on leave with related-target guards
  • nested hover transitions keep submenu open while pointer moves across trigger/content corridor
  • nested submenu selection closes every open ancestor menu in the same tree
  • floating position recalculates on viewport changes; vanilla adds a deferred first-open recalculation for stable initial placement

11. Keyboard & Focus

  • roving tabIndex on enabled items
  • Escape closes
  • Tab closes and keeps native focus traversal
  • React composition restores trigger focus on Escape close; vanilla keeps native focus behavior unless host code overrides it

12. ARIA

  • content: role=&quot;menu&quot;
  • item: role=&quot;menuitem&quot; and aria-disabled when disabled
  • trigger: aria-haspopup=&quot;menu&quot;, aria-expanded, aria-controls
  • content open/closed reflection via hidden + data-state

13. Validator Rules

  • structure.invalid-slot-placement
  • structure.missing-required-slot
  • keyboard.missing-required-keymap
  • keyboard.unhandled-typeahead
  • keyboard.unhandled-escape
  • focus.invalid-tabindex-roving

14. Adapter Notes

Vanilla

  • Primary binder: bindMenu.
  • Binds explicit DOM nodes and should be cleaned up on unmount.

15. Example Mapping

  • No direct example mapping yet.

16. API

Core

  • createMenuController value
  • MenuController type
  • MenuControllerOptions type
  • MenuItemRegistration type
  • MenuKeyboardResult type

Vanilla

  • bindMenu function
  • MenuBindingOptions interface
  • MenuElements interface
  • MenuPositioningPlacement type