@data-slot/navigation-menu

中文文档
1.1.3发布于 2026/11/05MITTypeScriptESM + CJS构建来源已验证近 30 天下载 2万

Headless navigation menu (mega menu) component for vanilla JavaScript. Accessible, unstyled, tiny.

原始使用文档
文档目录14 节
  1. Installation
  2. Quick Start
  3. API
  4. Initialization
  5. Slots
  6. Data Attributes
  7. Options
  8. Content mounting and server rendering
  9. Controller
  10. Events
  11. Styling
  12. Keyboard Navigation
  13. Behavior
  14. License

@data-slot/navigation-menu

Headless navigation menu (mega menu) component for vanilla JavaScript. Accessible, unstyled, tiny.

Installation

npm install @data-slot/navigation-menu

Quick Start

<nav data-slot="navigation-menu">
  <ul data-slot="navigation-menu-list">
    <li data-slot="navigation-menu-item" data-value="products">
      <button data-slot="navigation-menu-trigger">Products</button>
      <div data-slot="navigation-menu-content">
        <a href="/product-a">Product A</a>
        <a href="/product-b">Product B</a>
      </div>
    </li>
    <li data-slot="navigation-menu-item" data-value="company">
      <button data-slot="navigation-menu-trigger">Company</button>
      <div data-slot="navigation-menu-content">
        <a href="/about">About</a>
        <a href="/careers">Careers</a>
      </div>
    </li>
    <li data-slot="navigation-menu-indicator" role="presentation" aria-hidden="true"></li>
  </ul>
  <div data-slot="navigation-menu-portal">
    <div data-slot="navigation-menu-positioner">
      <div data-slot="navigation-menu-popup">
        <div data-slot="navigation-menu-viewport"></div>
      </div>
    </div>
  </div>
</nav>

<script type="module">
  import { create } from "@data-slot/navigation-menu";
  
  const controllers = create();
</script>

Minimal markup that only authors navigation-menu-viewport still works. The runtime generates the popup stack while the menu is open and restores the original DOM on close.

API

Initialization

create(scope?)

Auto-discover and bind all navigation menu instances in a scope (defaults to document).

import { create } from "@data-slot/navigation-menu";

const controllers = create(); // Returns NavigationMenuController[]

createNavigationMenu(root, options?)

Create a controller for a specific element.

import { createNavigationMenu } from "@data-slot/navigation-menu";

const menu = createNavigationMenu(element, {
  delayOpen: 0,
  delayClose: 0,
  onValueChange: (value) => console.log(value),
});

Slots

Runtime Slots

  • navigation-menu - Root navigation element that manages the active panel and open state.
  • navigation-menu-list - Required top-level list containing navigation items; used to discover top-level hover and focus targets.
  • navigation-menu-item - Container for a navigation entry; at least one item is required. Use data-value to identify an entry with a content panel.
  • navigation-menu-trigger - Button inside an item that opens its associated content panel.
  • navigation-menu-content - Content panel inside an item, displayed when that item is active.
  • navigation-menu-indicator - Optional animated highlight that follows top-level hover/focus targets and stays anchored to the active trigger while a panel is open.
  • navigation-menu-portal - Portal wrapper moved to document.body while the menu is open.
  • navigation-menu-positioner - Popup positioning wrapper that receives resolved side/alignment attributes and sizing variables.
  • navigation-menu-popup - Animated popup shell around the viewport.
  • navigation-menu-viewport - Optional clipping viewport that holds the active content panel.
  • navigation-menu-viewport-positioner - Deprecated alias for navigation-menu-positioner.

Generated Slots

  • navigation-menu-bridge - Runtime-created hover safety shield that keeps the menu open while crossing the gap to its content.
  • navigation-menu-safe-triangle - Runtime-created debug polygon that visualizes the hover safety corridor when debugging is enabled.

Markup

<nav data-slot="navigation-menu">
  <ul data-slot="navigation-menu-list">
    <li data-slot="navigation-menu-item" data-value="unique-id">
      <button data-slot="navigation-menu-trigger">Label</button>
      <div data-slot="navigation-menu-content">
        <!-- Links, content -->
      </div>
    </li>
    <!-- Optional hover indicator -->
    <li data-slot="navigation-menu-indicator" role="presentation" aria-hidden="true"></li>
  </ul>
  <div data-slot="navigation-menu-portal">
    <div data-slot="navigation-menu-positioner">
      <div data-slot="navigation-menu-popup">
        <div data-slot="navigation-menu-viewport"></div>
      </div>
    </div>
  </div>
</nav>

If you only author navigation-menu-viewport, the runtime synthesizes the missing navigation-menu-portal, navigation-menu-positioner, and navigation-menu-popup wrappers while the menu is open.

Data Attributes

Options can also be set via data attributes on the root element. JS options take precedence.

AttributeTypeDefaultDescription
data-delay-opennumber0Delay before opening on hover (ms)
data-delay-closenumber0Delay before closing on mouse leave (ms)
data-open-on-focusbooleanfalseWhether focusing a trigger opens its content
data-mount-strategy"lazy" | "eager""lazy"Detach inactive panels or keep them connected and hidden
data-sidestring"bottom"Side: "top", "right", "bottom", "left"
data-alignstring"start"Viewport alignment: "start", "center", or "end"
data-side-offsetnumber0Distance from trigger to viewport (px)
data-align-offsetnumber0Cross-axis alignment offset (px)
data-position-methodstring"absolute"Positioning strategy: "absolute" or "fixed"
data-safe-trianglebooleanfalseEnable hover safe-triangle switching guard
data-debug-safe-trianglebooleanfalseShow red hover safe-triangle debug overlay

Boolean attributes: present or "true" = true, "false" = false, absent = default.

Placement attributes (data-side, data-align, data-side-offset, data-align-offset) control how the viewport is positioned relative to the active trigger:

  • start - Align viewport left edge with trigger left edge (default)
  • center - Center viewport under trigger
  • end - Align viewport right edge with trigger right edge

Can be set on:

  1. navigation-menu-content (highest priority, per-panel)
  2. navigation-menu-item
  3. navigation-menu root (lowest priority, global default)

navigation-menu-positioner is the canonical popup positioning slot. Its data-side / data-align are mirrored output values and are not used as placement inputs. navigation-menu-viewport-positioner is still accepted as a legacy alias.

<!-- Faster hover response with focus auto-open opt-in -->
<nav data-slot="navigation-menu" data-delay-open="100" data-open-on-focus="true">
  ...
</nav>

<!-- Center-align a narrow submenu under its trigger -->
<li data-slot="navigation-menu-item" data-value="company" data-align="center">
  <button data-slot="navigation-menu-trigger">Company</button>
  <div data-slot="navigation-menu-content">
    <a href="/about">About</a>
    <a href="/careers">Careers</a>
  </div>
</li>

Output Attributes

SurfaceAttributes
rootdata-state="open|closed", data-open, data-closed
positionerdata-state="open|closed", data-open, data-closed, data-side, data-align, data-instant
popupdata-state="open|closed", data-open, data-closed, data-side, data-align, data-starting-style, data-ending-style, data-instant
viewportdata-state="open|closed", data-open, data-closed, data-side, data-align, data-starting-style, data-ending-style, data-instant
contentdata-state="active|inactive", data-open, data-closed, data-side, data-align, data-starting-style, data-ending-style, data-activation-direction="left|right"

data-activation-direction is only emitted while switching between open top-level panels. A full close is intentionally non-directional.

Options

OptionTypeDefaultDescription
delayOpennumber0Delay before opening on hover (ms)
delayClosenumber0Delay before closing on mouse leave (ms)
openOnFocusbooleanfalseWhether focusing a trigger opens its content
mountStrategy"lazy" | "eager""lazy"Detach inactive panels or keep them connected and hidden
side"top" | "right" | "bottom" | "left""bottom"Viewport side relative to trigger
align"start" | "center" | "end""start"Viewport alignment on cross-axis
sideOffsetnumber0Distance from trigger to viewport (px)
alignOffsetnumber0Cross-axis alignment offset (px)
positionMethod"absolute" | "fixed""absolute"Positioning strategy for the shared popup positioner
safeTrianglebooleanfalseEnable hover safe-triangle switching guard
onValueChange(value: string | null) => voidundefinedCallback when active item changes
debugSafeTrianglebooleanfalseShow red hover safe-triangle debug overlay

Content mounting and server rendering

By default, Navigation Menu removes its closed panels from the page’s DOM. This keeps pages with large menus smaller after initialization. The menu buttons and links outside those panels stay in the page.

Opening a panel puts the same content back into the page. Its state and event listeners are preserved. When switching panels, the new panel appears while the previous one finishes its exit animation. After that animation, the previous panel is removed.

Closing the menu waits for the panels and their surrounding popup to finish their exit animations. Reopening a panel during its exit animation keeps it in the page.

Most applications can use this default without any changes.

If your code needs to find or update links inside a closed panel—for example, using querySelector—use eager mode to keep the panels in the DOM:

createNavigationMenu(root, { mountStrategy: "eager" });

You can also add data-mount-strategy="eager" to the Navigation Menu root. A JavaScript option takes precedence over the HTML attribute.

With the default lazy mode, DOM queries won’t find closed panels or their links. You can also keep a reference to a panel before initializing Navigation Menu and use that reference later.

Server rendering and pages without JavaScript

Lazy mounting does not reduce the HTML sent to the browser. Panel content is still included in your HTML; Navigation Menu removes closed panels from the DOM after JavaScript initializes.

Make sure essential navigation works before JavaScript loads or if it never runs. You can leave the links visible until initialization, or provide separate fallback navigation if your panels start hidden. Neither lazy nor eager mode provides that fallback automatically.

Controller

Method/PropertyDescription
open(value)Open a specific item
close()Close the menu
valueCurrently active item value (readonly string | null)
destroy()Cleanup all event listeners

Controller Destruction

destroy() permanently disposes the controller and hides any open surface without emitting an additional change event. Repeated destruction is safe; methods on the old controller become no-ops. Create a new controller on the same root to rebind it.

Events

Outbound Events

Listen for changes via custom events:

element.addEventListener("navigation-menu:change", (e) => {
  console.log("Active item:", e.detail.value);
});

Inbound Events

Control the navigation menu via events:

EventDetailDescription
navigation-menu:set{ value: string | null }Set active item or close menu
// Open a specific item
element.dispatchEvent(
  new CustomEvent("navigation-menu:set", { detail: { value: "products" } })
);

// Close the menu
element.dispatchEvent(
  new CustomEvent("navigation-menu:set", { detail: { value: null } })
);

Styling

navigation-menu-portal is moved to document.body while open. If authored popup-stack slots are present, they are reused. Otherwise, missing portal / positioner / popup wrappers are generated while open and removed on close.

Runtime geometry is owned by navigation-menu-positioner: position, top, left, width, and height are written inline and reset on close. navigation-menu-popup is the canonical animated shell. The active navigation-menu-content panel is mounted inside navigation-menu-viewport while open. Inactive panels are detached after their exit animation by default; eager panels return to their original markup location.

Use positionMethod: "fixed" or data-position-method="fixed" when the menu needs viewport-based anchoring, such as inside sticky headers.

Basic Styling

/* Hidden by default */
[data-slot="navigation-menu-content"] {
  display: none;
}

[data-slot="navigation-menu-content"][data-open] {
  display: block;
}

/* Positioner owns placement geometry */
[data-slot="navigation-menu-positioner"],
[data-slot="navigation-menu-viewport-positioner"] {
  top: 0;
  left: 0;
  transition: top 0.3s, left 0.3s;
}

/* Skip initial position animation */
[data-slot="navigation-menu-positioner"][data-instant],
[data-slot="navigation-menu-viewport-positioner"][data-instant] {
  transition: none;
}

/* Popup owns the main shell animation */
[data-slot="navigation-menu-popup"] {
  transform-origin: var(--transform-origin);
  width: var(--popup-width);
  height: var(--popup-height);
  transition: transform 0.3s, width 0.3s, height 0.3s, opacity 0.15s;
}

[data-slot="navigation-menu-popup"][data-starting-style],
[data-slot="navigation-menu-popup"][data-ending-style] {
  opacity: 0;
}

/* Viewport keeps backward-compatible sizing aliases */
[data-slot="navigation-menu-viewport"] {
  width: var(--viewport-width);
  height: var(--viewport-height);
}

/* Skip animation on initial open */
[data-slot="navigation-menu-viewport"][data-instant] {
  transition: none;
}

/* Indicator positioning */
[data-slot="navigation-menu-indicator"] {
  position: absolute;
  left: var(--indicator-left);
  width: var(--indicator-width);
  transition: left 0.2s, width 0.2s;
}

data-instant is used for the initial open phase and for sync-driven anchor tracking updates (for example when a sticky trigger moves while the menu is open). It is cleared when switching, closing, or after the tracking update settles. Use it to skip those reposition transitions without suppressing exit animations.

Motion Animations

Use data-starting-style / data-ending-style on navigation-menu-popup for full popup open/close animations, and on navigation-menu-content for panel presence. Directional panel switching should key off data-activation-direction="left|right" on content. The active panel stays in normal flow; mounted inactive/exiting panels are absolutely positioned by the runtime so they do not affect popup measurement while they animate out.

For exit animations, avoid CSS that force-hides content immediately by data-state (for example display: none on non-active panels), because that bypasses the presence lifecycle.

[data-slot="navigation-menu-popup"][data-starting-style],
[data-slot="navigation-menu-popup"][data-ending-style] {
  opacity: 0;
  transform: scale(0.96);
}

[data-slot="navigation-menu-content"][data-starting-style],
[data-slot="navigation-menu-content"][data-ending-style] {
  opacity: 0;
}

[data-slot="navigation-menu-content"][data-starting-style][data-activation-direction="right"] {
  transform: translateX(2rem);
}

[data-slot="navigation-menu-content"][data-starting-style][data-activation-direction="left"] {
  transform: translateX(-2rem);
}

[data-slot="navigation-menu-content"][data-ending-style][data-activation-direction="right"] {
  transform: translateX(-2rem);
}

[data-slot="navigation-menu-content"][data-ending-style][data-activation-direction="left"] {
  transform: translateX(2rem);
}

Legacy data-motion and --motion-direction are still emitted during panel switches for backward compatibility. That includes both root-level and content-level data-motion output, but data-activation-direction is the canonical directional hook.

--popup-width / --popup-height are the popup shell size vars. While the menu is open, they stay at fixed pixel values so CSS can animate panel-to-panel size changes directly between px targets. Initial open writes the measured size synchronously; the shell does not need an auto reset while open.

CSS Variables

VariableElementDescription
--popup-widthpopupPopup width while open; CSS can animate shell width between pixel values
--popup-heightpopupPopup height while open; CSS can animate shell height between pixel values
--positioner-widthpositionerMeasured width of the active panel
--positioner-heightpositionerMeasured height of the active panel
--available-widthpositionerAvailable width between the active trigger and the viewport edge
--available-heightpositionerAvailable height between the active trigger and the viewport edge
--transform-originpopup / viewport / content / positionerPixel origin anchored to trigger (side + align), scoped to each element's coordinate space
--viewport-widthviewportLegacy alias for the active panel width
--viewport-heightviewportLegacy alias for the active panel height
--indicator-leftindicatorLeft offset from list
--indicator-widthindicatorWidth of hovered trigger
--indicator-topindicatorTop offset from list
--indicator-heightindicatorHeight of hovered trigger
--motion-directionviewportLegacy switching direction output: 1 (right) or -1 (left)

Deprecated Compatibility

  • navigation-menu-viewport-positioner is a deprecated alias for navigation-menu-positioner.
  • --viewport-width, --viewport-height, root/content data-motion, and --motion-direction are deprecated and planned for removal in the next major release.
  • Content-wrapped navigation-menu-portal / navigation-menu-positioner shells are restore-only compatibility and are also planned for removal in the next major release.

Keyboard Navigation

Within Top-Level Items

KeyAction
ArrowLeftMove focus to previous top-level item (submenu trigger or plain link)
ArrowRightMove focus to next top-level item (submenu trigger or plain link)
TabMove focus to next top-level item in DOM order
Shift+TabMove focus to previous top-level item in reverse DOM order
ArrowDownMove focus into content panel (only when focused item has submenu content)
HomeMove focus to first top-level item
EndMove focus to last top-level item
EscapeClose menu

Top-level submenu triggers and plain links remain in the natural tab order.

Within Content Panel

KeyAction
ArrowDown / ArrowRightMove to next focusable element
ArrowUp / ArrowLeftMove to previous element (returns to trigger at start)
TabFrom last content item, move focus to next top-level nav item; if none, move to next focusable after nav root
Shift+TabFrom first content item, move focus back to owning trigger
EscapeClose menu and return focus to trigger

Behavior

  • Hover: Opens after delayOpen ms, closes after delayClose ms
  • Click: Locks menu open until explicit action (click same trigger, click another trigger, click outside, or Escape); hover does not switch/close while locked
  • Focus: Does not auto-open by default; set openOnFocus / data-open-on-focus="true" to opt in
  • Trigger open focus:
    • Pointer click/tap keeps focus on the trigger
    • Keyboard activation/programmatic click moves focus into menu content (first focusable item, or content panel fallback)
  • Indicator: Plain top-level links participate in indicator positioning when no submenu is open; open submenu state takes precedence
  • Switching: Instant transition between items (no delay)

License

MIT

版本与发布记录

@data-slot/navigation-menu 共发布 164 个版本,首次发布于 2026/01/09;过去 12 个月发布了 164 个版本。最新版本附带 npm provenance 构建来源证明,可追溯到公开的 CI 构建记录。

版本发布日期距今
1.1.3latest2026/11/056 天前
1.1.22026/10/1428 天前
1.1.12026/10/051 个月前
1.1.02026/10/021 个月前
1.0.22026/09/271 个月前
1.0.12026/09/261 个月前

关于 @data-slot/navigation-menu 的常见问题

@data-slot/navigation-menu 是什么?

Headless navigation menu (mega menu) component for vanilla JavaScript. Accessible, unstyled, tiny.

如何安装 @data-slot/navigation-menu?

在项目目录运行 npm install @data-slot/navigation-menu,或使用 pnpm add / yarn add / bun add。

@data-slot/navigation-menu 支持 TypeScript 吗?

支持。包内自带 TypeScript 类型声明(package.json 声明了 types 入口),安装后即可获得类型提示,无需再安装 @types 包。

@data-slot/navigation-menu 支持 ESM 还是 CommonJS?

同时提供 ES Module 与 CommonJS 入口,import 和 require() 都可以使用,打包工具会自动选择合适的版本。

@data-slot/navigation-menu 可以商用吗?

@data-slot/navigation-menu 使用 MIT 许可。宽松许可:允许商业使用、修改和再分发,通常只需在分发时保留原版权与许可声明。以上为概括,不构成法律意见,请以许可原文为准。

@data-slot/navigation-menu 的下载量有多少?

根据 npm 官方下载统计,@data-slot/navigation-menu 在 2026/10/12 – 2026/11/10 这 30 天内被下载 19,684 次,日均约 656 次。下载量会把 CI 构建、镜像同步和作为其他包依赖的安装一并计入,适合横向比较热度,不能直接等同于使用人数。

@data-slot/navigation-menu 还在维护吗?

最新版本 1.1.3 发布于 2026/11/05(6 天前);至今共发布 164 个版本,其中过去 12 个月发布了 164 个;首次发布于 2026/01/09。发布频率只是参考信号:功能稳定的工具可能很少需要更新,建议结合源码仓库的 issue 和提交记录判断。