@data-slot/dropdown-menu

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

Headless dropdown menu for vanilla JavaScript. Supports action items, single-select radio items, and multi-select checkbox items with full keyboard navigation and ARIA support.

原始使用文档
文档目录17 节
  1. Installation
  2. Usage
  3. Action Menu
  4. Single-Select Menu
  5. Multi-Select Menu
  6. JavaScript
  7. API
  8. Initialization
  9. Slots
  10. Data Attributes
  11. Options
  12. Controller
  13. Events
  14. Styling
  15. Keyboard Navigation
  16. Migration Notes
  17. License

@data-slot/dropdown-menu

Headless dropdown menu for vanilla JavaScript. Supports action items, single-select radio items, and multi-select checkbox items with full keyboard navigation and ARIA support.

Installation

npm install @data-slot/dropdown-menu

Usage

Action Menu

<div data-slot="dropdown-menu">
  <button data-slot="dropdown-menu-trigger">Actions</button>
  <div data-slot="dropdown-menu-content">
    <button data-slot="dropdown-menu-item" data-value="edit">Edit</button>
    <button data-slot="dropdown-menu-item" data-value="copy">Copy</button>
    <button data-slot="dropdown-menu-item" data-variant="destructive" data-value="delete">
      Delete
    </button>
  </div>
</div>

Single-Select Menu

<div data-slot="dropdown-menu" data-default-value="pro">
  <button data-slot="dropdown-menu-trigger">Plan</button>
  <div data-slot="dropdown-menu-content">
    <button data-slot="dropdown-menu-radio-item" data-value="starter">Starter</button>
    <button data-slot="dropdown-menu-radio-item" data-value="pro">Pro</button>
    <button data-slot="dropdown-menu-radio-item" data-value="team">Team</button>
  </div>
</div>

Multi-Select Menu

<div
  data-slot="dropdown-menu"
  data-close-on-select="false"
  data-default-values='["email","push"]'
>
  <button data-slot="dropdown-menu-trigger">Channels</button>
  <div data-slot="dropdown-menu-content">
    <button data-slot="dropdown-menu-checkbox-item" data-value="email">Email</button>
    <button data-slot="dropdown-menu-checkbox-item" data-value="sms">SMS</button>
    <button data-slot="dropdown-menu-checkbox-item" data-value="push">Push</button>
  </div>
</div>

JavaScript

import { create, createDropdownMenu } from "@data-slot/dropdown-menu";

const controllers = create();

const root = document.querySelector('[data-slot="dropdown-menu"]');
const controller = createDropdownMenu(root, {
  onOpenChange: (open) => console.log("open:", open),
  onSelect: (value) => console.log("activation:", value),
  onValueChange: (value) => console.log("radio value:", value),
  onValuesChange: (values) => console.log("checkbox values:", values),
});

controller.set({ value: "pro" });
controller.set({ values: ["email", "push"] });
controller.set({ open: true, highlightedValue: "push" });
controller.destroy();

createDropdownMenu(root) is idempotent per root. Calling it again for the same element returns the existing controller.

API

Initialization

create(scope?)

Find and bind uninitialized [data-slot="dropdown-menu"] descendants of scope (defaults to document). Returns DropdownMenuController[] for newly bound roots. To initialize the scope element itself, use createDropdownMenu.

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

const controllers = create();

createDropdownMenu(root, options?)

Create a DropdownMenuController for one root element. JavaScript options take precedence over the corresponding data attributes. Calling this again for a bound root returns its existing controller; destroy it before rebinding with new options.

import { createDropdownMenu } from "@data-slot/dropdown-menu";

const controller = createDropdownMenu(element, {});

Slots

Runtime Slots

  • dropdown-menu - Root container.
  • dropdown-menu-trigger - Required button that toggles the menu and anchors its position.
  • dropdown-menu-content - Required menu panel containing action and selection items.
  • dropdown-menu-item - Action item with no owned selection state.
  • dropdown-menu-radio-item - Single-select menu item.
  • dropdown-menu-checkbox-item - Multi-select menu item.
  • dropdown-menu-positioner - Optional authored positioning wrapper.
  • dropdown-menu-portal - Optional authored portal wrapper that contains dropdown-menu-positioner.

Style-only Slots

  • dropdown-menu-group - Groups related items.
  • dropdown-menu-label - Non-interactive label.
  • dropdown-menu-separator - Visual divider.
  • dropdown-menu-shortcut - Optional keyboard shortcut hint; displaying it does not register a keyboard shortcut.

Composed Portal Markup

<div data-slot="dropdown-menu">
  <button data-slot="dropdown-menu-trigger">Options</button>
  <div data-slot="dropdown-menu-portal">
    <div data-slot="dropdown-menu-positioner">
      <div data-slot="dropdown-menu-content">...</div>
    </div>
  </div>
</div>

Data Attributes

Root and Content

AttributeTargetDescription
data-state="open|closed"root, contentCurrent open state
data-open / data-closedroot, contentPresence aliases for state styling
data-value="..."rootCurrent committed radio value only
data-sidecontent, positionerComputed side after collision handling
data-aligncontent, positionerComputed alignment after collision handling

Items

AttributeTargetDescription
data-highlighteditemCurrent highlighted item
data-checkedradio, checkbox itemsCurrent committed checked state
data-disableditemDisabled item
data-variantitemStyling hook such as destructive
data-insetitemStyling hook for left padding

Defaults and Options

AttributeTargetDescription
data-default-openrootInitial open state
data-default-valuerootInitial radio value
data-default-values='["a","b"]'rootInitial checkbox values as a JSON array string
data-default-checkedradio, checkbox itemItem-level default checked state
data-close-on-click-outsiderootClose on outside interaction
data-close-on-escaperootClose on Escape
data-close-on-selectrootClose after accepted activation
data-highlight-item-on-hoverrootHighlight and focus items on hover

Default precedence is:

  1. JavaScript options
  2. Root data attributes
  3. Item data-default-checked
  4. Empty state

For radio items, root defaults win over item defaults. For checkbox items, root data-default-values wins over item data-default-checked.

Options

OptionData AttributeTypeDefaultDescription
defaultOpendata-default-openbooleanfalseInitial open state
defaultValuedata-default-valuestring | nullItem defaults, then nullInitial radio value; explicit null clears item defaults
defaultValuesdata-default-valuesstring[]Item defaults, then []Initial checkbox values; encode the attribute as a JSON array
closeOnClickOutsidedata-close-on-click-outsidebooleantrueClose on outside interaction
closeOnEscapedata-close-on-escapebooleantrueClose on Escape
closeOnSelectdata-close-on-selectbooleantrueClose after accepted user activation
highlightItemOnHoverdata-highlight-item-on-hoverbooleantrueHighlight and focus items on hover
sidedata-side"top" | "right" | "bottom" | "left""bottom"Preferred popup side
aligndata-align"start" | "center" | "end""start"Preferred popup alignment
sideOffsetdata-side-offsetnumber4Distance from trigger (px)
alignOffsetdata-align-offsetnumber0Alignment offset (px)
avoidCollisionsdata-avoid-collisionsbooleantrueFlip and shift within the viewport
collisionPaddingdata-collision-paddingnumber8Viewport edge padding (px)
lockScrolldata-lock-scrollbooleantrueLock page scroll while open; uses fixed positioning when enabled and absolute positioning when disabled
onOpenChange—(open: boolean) => voidundefinedCalled when open state changes
onSelect—(value: string) => voidundefinedCalled after accepted user activation
onValueChange—(value: string | null) => voidundefinedCalled when committed radio selection changes
onValuesChange—(values: string[]) => voidundefinedCalled when committed checkbox selection changes

Notes:

  • closeOnSelect defaults to true. Multi-select menus usually want false.
  • onSelect tracks accepted user activation. It does not fire for programmatic state changes.
  • onValueChange and onValuesChange follow the same silence rules as their DOM events.

Controller

interface DropdownMenuController {
  open(): void;
  close(): void;
  toggle(): void;
  set(detail: DropdownMenuSetDetail): void;
  readonly isOpen: boolean;
  readonly value: string | null;
  readonly values: string[];
  readonly highlightedValue: string | null;
  destroy(): void;
}

DropdownMenuSetDetail is a partial update:

interface DropdownMenuSetDetail {
  open?: boolean;
  value?: string | null;
  values?: string[];
  highlightedValue?: string | null;
  source?: "programmatic" | "restore";
}

Use value: null to clear radio selection, values: [] to clear checkbox selection, and highlightedValue: null to clear the highlight. source defaults to "programmatic".

set() applies fields in this order: value, values, open, highlightedValue.

  • set({ value }) commits radio selection.
  • set({ values }) commits checkbox selection.
  • set({ open }) opens or closes the menu.
  • set({ highlightedValue }) updates highlight only while the menu is open.
  • Programmatic set() never emits dropdown-menu:select.
  • No-op updates are silent.
  • Unknown value / values targets are ignored, not thrown.

Downstream Wrapper Contract

This package does not ship Astro components, but downstream wrappers should mirror this authoring model:

  • DropdownMenuItem renders data-slot="dropdown-menu-item".
  • DropdownMenuRadioItem renders data-slot="dropdown-menu-radio-item" and requires value.
  • DropdownMenuCheckboxItem renders data-slot="dropdown-menu-checkbox-item" and requires value.
  • Radio and checkbox wrappers may expose defaultChecked, but root defaults still take precedence over item defaults.

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.

Focus restoration already queued by a close survives destruction.

Events

Outbound Events

EventDetailNotes
dropdown-menu:open-change{ open, previousOpen, source, reason }Fires on real open-state changes
dropdown-menu:changesame detailDeprecated alias for open-change; only detail.open is compatibility-guaranteed
dropdown-menu:highlight-change{ value, previousValue, item, previousItem, source }value and item become null when highlight clears
dropdown-menu:select{ value, item, itemType, source, checked? }Cancelable, user-only, fires before commit
dropdown-menu:value-change{ value, previousValue, item, previousItem, source }Radio commits only
dropdown-menu:values-change{ values, previousValues, changedValue, checked, item, source }Checkbox commits only

dropdown-menu:select behavior:

  • Fires only for user activation attempts. Disabled items and programmatic updates do not emit it.
  • If event.preventDefault() is called, no selection state changes and no auto-close from selection occur.
  • Programmatic updates never emit it.

dropdown-menu:value-change, dropdown-menu:values-change, onValueChange, and onValuesChange are silent on:

  • initialization
  • no-op commits
  • ignored unknown targets

For dropdown-menu:values-change, changedValue, checked, and item are null when one programmatic update changes more than one checkbox at once.

Inbound Events

EventDetailDescription
dropdown-menu:setDropdownMenuSetDetailPartial programmatic state update
root.dispatchEvent(
  new CustomEvent("dropdown-menu:set", {
    detail: { value: "pro", source: "restore" },
  })
);

root.dispatchEvent(
  new CustomEvent("dropdown-menu:set", {
    detail: { values: ["email", "push"], open: true },
  })
);

Event Order

User radio selection:

  1. dropdown-menu:select
  2. dropdown-menu:value-change
  3. dropdown-menu:open-change if the menu closes

User checkbox selection:

  1. dropdown-menu:select
  2. dropdown-menu:values-change
  3. dropdown-menu:open-change if the menu closes

Programmatic selection:

  1. dropdown-menu:set or controller.set(...)
  2. dropdown-menu:value-change or dropdown-menu:values-change
  3. No dropdown-menu:select

If closing the menu also clears an existing highlight, dropdown-menu:highlight-change is emitted before the close-side dropdown-menu:open-change.

Styling

Positioning

Placement attributes (data-side, data-align, data-side-offset, data-align-offset, data-avoid-collisions, data-collision-padding) resolve in this order:

  1. JavaScript option
  2. dropdown-menu-content
  3. dropdown-menu-positioner
  4. dropdown-menu root

The dropdown menu uses position: fixed by default and automatically positions itself relative to the trigger:

createDropdownMenu(root, {
  side: "bottom",
  align: "start",
  sideOffset: 4,
  alignOffset: 0,
  avoidCollisions: true,
  collisionPadding: 8,
});

When avoidCollisions is enabled, the menu may flip sides or shift within the viewport. The positioned element also receives --transform-origin for animation origins.

Keyboard Navigation

KeyAction
Enter / SpaceOpen menu from trigger, or activate highlighted item
ArrowDownOpen menu from trigger, or move to next enabled item
ArrowUpMove to previous enabled item
HomeMove to first enabled item
EndMove to last enabled item
EscapeClose menu
TabClose menu and continue tab order
A-ZTypeahead by item text

Migration Notes

If you currently use dropdown-menu as a picker:

  • Replace picker-style dropdown-menu-item usage with dropdown-menu-radio-item or dropdown-menu-checkbox-item.
  • Stop manually writing data-selected or data-checked.
  • Listen to dropdown-menu:value-change or dropdown-menu:values-change for committed state.
  • Push external restore/randomize/popstate/storage changes back in through controller.set(...) or dropdown-menu:set.
  • Switch open-state listeners from dropdown-menu:change to dropdown-menu:open-change.

Deprecated APIs

The following compatibility APIs are deprecated and will be removed in the next major release:

// Deprecated open-state alias
root.addEventListener("dropdown-menu:change", (event) => {
  console.log(event.detail.open);
});

// Deprecated programmatic shape
root.dispatchEvent(
  new CustomEvent("dropdown-menu:set", { detail: { value: true } })
);

Use dropdown-menu:open-change and dropdown-menu:set { open: boolean } instead.

License

MIT

版本与发布记录

@data-slot/dropdown-menu 共发布 157 个版本,首次发布于 2026/01/19;过去 12 个月发布了 157 个版本。最新版本附带 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/dropdown-menu 的常见问题

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

Headless dropdown menu for vanilla JavaScript. Supports action items, single-select radio items, and multi-select checkbox items with full keyboard navigation and ARIA support.

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

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

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

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

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

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

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

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

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

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

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

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