Dropdown Menu
@cloudflare/kumo
import { DropdownMenu, Button } from "@cloudflare/kumo";
import { PlusIcon } from "@phosphor-icons/react";

export function DropdownBasicDemo() {
  return (
    <DropdownMenu>
      <DropdownMenu.Trigger render={<Button icon={PlusIcon}>Add</Button>} />
      <DropdownMenu.Content>
        <DropdownMenu.Item>Worker</DropdownMenu.Item>
        <DropdownMenu.Item>Pages</DropdownMenu.Item>
        <DropdownMenu.Item>KV Namespace</DropdownMenu.Item>
      </DropdownMenu.Content>
    </DropdownMenu>
  );
}

安装

批量导入

import { DropdownMenu } from "@cloudflare/kumo";

按需导入

import { DropdownMenu } from "@cloudflare/kumo/components/dropdown";

用法

import { DropdownMenu, Button } from "@cloudflare/kumo";

export default function Example() {
  return (
    <DropdownMenu>
      <DropdownMenu.Trigger render={<Button>Menu</Button>} />
      <DropdownMenu.Content>
        <DropdownMenu.Item onClick={() => console.log("edit")}>
          Edit
        </DropdownMenu.Item>
        <DropdownMenu.Item onClick={() => console.log("duplicate")}>
          Duplicate
        </DropdownMenu.Item>
        <DropdownMenu.Separator />
        <DropdownMenu.Item
          variant="danger"
          onClick={() => console.log("delete")}
        >
          Delete
        </DropdownMenu.Item>
      </DropdownMenu.Content>
    </DropdownMenu>
  );
}

示例

基本下拉

import { DropdownMenu, Button } from "@cloudflare/kumo";
import { PlusIcon } from "@phosphor-icons/react";

export function DropdownBasicDemo() {
  return (
    <DropdownMenu>
      <DropdownMenu.Trigger render={<Button icon={PlusIcon}>Add</Button>} />
      <DropdownMenu.Content>
        <DropdownMenu.Item>Worker</DropdownMenu.Item>
        <DropdownMenu.Item>Pages</DropdownMenu.Item>
        <DropdownMenu.Item>KV Namespace</DropdownMenu.Item>
      </DropdownMenu.Content>
    </DropdownMenu>
  );
}

缩进条目

当菜单中的任一条目带有图标时,没有图标的条目(包括标签)会自动缩进, 使它们的文本对齐。当自动检测无法看到图标时(例如条目通过 render 渲染自己的内容),请使用 inset 强制缩进。

import { DropdownMenu, Button } from "@cloudflare/kumo";
import { CopyIcon, PencilSimpleIcon, TrashIcon } from "@phosphor-icons/react";

/**
 * Items without an icon are inset automatically when another item in the same
 * menu has one, so their text lines up.
 */
export function DropdownInsetDemo() {
  return (
    <DropdownMenu>
      <DropdownMenu.Trigger render={<Button>Edit</Button>} />
      <DropdownMenu.Content>
        <DropdownMenu.Item icon={PencilSimpleIcon}>Rename</DropdownMenu.Item>
        <DropdownMenu.Item icon={CopyIcon}>Duplicate</DropdownMenu.Item>
        <DropdownMenu.Separator />
        <DropdownMenu.Item>Move to folder</DropdownMenu.Item>
        <DropdownMenu.Item>Add to favorites</DropdownMenu.Item>
        <DropdownMenu.Separator />
        <DropdownMenu.Item icon={TrashIcon} variant="danger">
          Delete
        </DropdownMenu.Item>
      </DropdownMenu.Content>
    </DropdownMenu>
  );
}

处理条目点击

使用 DropdownMenu.Item 上的 onClick 处理操作。每个条目都会接收到标准的 React 鼠标事件处理器。

import { useState } from "react";
import { DropdownMenu, Button } from "@cloudflare/kumo";
import { ArrowsClockwiseIcon, CopyIcon, PencilSimpleIcon, TrashIcon } from "@phosphor-icons/react";

/**
 * Use `onClick` on `DropdownMenu.Item` to handle item actions.
 * Each item receives a standard React mouse event handler.
 */
export function DropdownOnClickDemo() {
  const [lastAction, setLastAction] = useState<string | null>(null);

  return (
    <div className="flex flex-col items-start gap-2">
      <DropdownMenu>
        <DropdownMenu.Trigger render={<Button>Actions</Button>} />
        <DropdownMenu.Content>
          <DropdownMenu.Item
            icon={CopyIcon}
            onClick={() => setLastAction("Duplicated")}
          >
            Duplicate
          </DropdownMenu.Item>
          <DropdownMenu.Item
            icon={PencilSimpleIcon}
            onClick={() => setLastAction("Renamed")}
          >
            Rename
          </DropdownMenu.Item>
          <DropdownMenu.Item
            icon={<ArrowsClockwiseIcon className="animate-spin" />}
            onClick={() => setLastAction("Refreshed")}
          >
            Refresh
          </DropdownMenu.Item>
          <DropdownMenu.Separator />
          <DropdownMenu.Item
            icon={TrashIcon}
            variant="danger"
            onClick={() => setLastAction("Deleted")}
          >
            Delete
          </DropdownMenu.Item>
        </DropdownMenu.Content>
      </DropdownMenu>
      {lastAction && (
        <p className="text-sm text-kumo-subtle">
          Last action: <span className="text-kumo-default">{lastAction}</span>
        </p>
      )}
    </div>
  );
}

复选框条目

使用 DropdownMenu.CheckboxItem 实现可独立勾选或取消勾选的可切换选项。

import { useState } from "react";
import { DropdownMenu, Button } from "@cloudflare/kumo";

export function DropdownCheckboxDemo() {
  const [showSidebar, setShowSidebar] = useState(true);
  const [showLineNumbers, setShowLineNumbers] = useState(false);
  const [wordWrap, setWordWrap] = useState(true);

  return (
    <DropdownMenu>
      <DropdownMenu.Trigger render={<Button>View Options</Button>} />
      <DropdownMenu.Content>
        <DropdownMenu.Group>
          <DropdownMenu.Label>Display</DropdownMenu.Label>
          <DropdownMenu.CheckboxItem
            checked={showSidebar}
            onCheckedChange={setShowSidebar}
          >
            Show sidebar
          </DropdownMenu.CheckboxItem>
          <DropdownMenu.CheckboxItem
            checked={showLineNumbers}
            onCheckedChange={setShowLineNumbers}
          >
            Show line numbers
          </DropdownMenu.CheckboxItem>
          <DropdownMenu.CheckboxItem
            checked={wordWrap}
            onCheckedChange={setWordWrap}
          >
            Word wrap
          </DropdownMenu.CheckboxItem>
        </DropdownMenu.Group>
      </DropdownMenu.Content>
    </DropdownMenu>
  );
}

带单选的内嵌菜单

使用 DropdownMenu.Sub、DropdownMenu.SubTrigger 和 DropdownMenu.SubContent 创建嵌套子菜单。对于语言、时区等单选列表, 请使用 DropdownMenu.RadioGroup 和 DropdownMenu.RadioItem。

import { useState } from "react";
import { DropdownMenu, Button } from "@cloudflare/kumo";
import { UserIcon, CreditCardIcon, MoonIcon, SignOutIcon, GlobeIcon, ClockIcon } from "@phosphor-icons/react";

export function DropdownNestedDemo() {
  const [language, setLanguage] = useState("en");
  const [timezone, setTimezone] = useState("America/Los_Angeles");

  return (
    <DropdownMenu>
      <DropdownMenu.Trigger render={<Button icon={UserIcon}>Account</Button>} />
      <DropdownMenu.Content>
        <DropdownMenu.Item icon={UserIcon}>Profile</DropdownMenu.Item>
        <DropdownMenu.Item icon={CreditCardIcon}>Billing</DropdownMenu.Item>
        <DropdownMenu.Item icon={MoonIcon}>Dark mode</DropdownMenu.Item>

        {/* Language submenu with RadioGroup */}
        <DropdownMenu.Sub>
          <DropdownMenu.SubTrigger icon={GlobeIcon}>
            Language
          </DropdownMenu.SubTrigger>
          <DropdownMenu.SubContent>
            <DropdownMenu.Group>
              <DropdownMenu.RadioGroup
                value={language}
                onValueChange={setLanguage}
              >
                {languages.map((lang) => (
                  <DropdownMenu.RadioItem key={lang.code} value={lang.code}>
                    {lang.label}
                    <DropdownMenu.RadioItemIndicator />
                  </DropdownMenu.RadioItem>
                ))}
              </DropdownMenu.RadioGroup>
            </DropdownMenu.Group>
          </DropdownMenu.SubContent>
        </DropdownMenu.Sub>

        {/* Timezone submenu with RadioGroup */}
        <DropdownMenu.Sub>
          <DropdownMenu.SubTrigger icon={<ClockIcon weight="duotone" />}>
            Set Timezone
          </DropdownMenu.SubTrigger>
          <DropdownMenu.SubContent>
            <DropdownMenu.Group>
              <DropdownMenu.RadioGroup
                value={timezone}
                onValueChange={setTimezone}
              >
                {timezones.map((tz) => (
                  <DropdownMenu.RadioItem key={tz.value} value={tz.value}>
                    {tz.label}
                    <DropdownMenu.RadioItemIndicator />
                  </DropdownMenu.RadioItem>
                ))}
              </DropdownMenu.RadioGroup>
            </DropdownMenu.Group>
          </DropdownMenu.SubContent>
        </DropdownMenu.Sub>

        <DropdownMenu.Separator />
        <DropdownMenu.Item icon={SignOutIcon} variant="danger">
          Log out
        </DropdownMenu.Item>
      </DropdownMenu.Content>
    </DropdownMenu>
  );
}

带头像的自定义触发器

使用 render 属性自定义触发器元素,同时传入要渲染在其中的子内容。 当需要将头像等非按钮触发器包裹在无障碍按钮元素中时,此功能非常实用。

import { DropdownMenu } from "@cloudflare/kumo";
import { UserIcon, SignOutIcon, GearIcon } from "@phosphor-icons/react";

/**
 * Demonstrates using the render prop with children to compose a custom trigger
 * that contains other elements. The render prop provides the trigger element,
 * while children are rendered inside it.
 */
export function DropdownAvatarTriggerDemo() {
  return (
    <DropdownMenu>
      <DropdownMenu.Trigger
        render={<button type="button" className="rounded-full" />}
      >
        <span className="flex h-8 w-8 items-center justify-center rounded-full bg-kumo-brand text-sm font-medium text-white">
          MR
        </span>
      </DropdownMenu.Trigger>
      <DropdownMenu.Content>
        <DropdownMenu.Item icon={UserIcon}>Profile</DropdownMenu.Item>
        <DropdownMenu.Item icon={GearIcon}>Settings</DropdownMenu.Item>
        <DropdownMenu.Separator />
        <DropdownMenu.Item icon={SignOutIcon} variant="danger">
          Log out
        </DropdownMenu.Item>
      </DropdownMenu.Content>
    </DropdownMenu>
  );
}

导航链接

使用 DropdownMenu.LinkItem 处理导航到 URL 的菜单条目。 它会渲染语义化的 <a> 元素,可完全控制 target、rel 等链接属性。

import { DropdownMenu, Button } from "@cloudflare/kumo";
import { GearIcon, BookOpenIcon, ArrowSquareOutIcon } from "@phosphor-icons/react";

/**
 * Demonstrates the new LinkItem component for navigation links.
 * Use LinkItem instead of Item with href for cleaner, more semantic links.
 */
export function DropdownLinkItemDemo() {
  return (
    <DropdownMenu>
      <DropdownMenu.Trigger render={<Button>Resources</Button>} />
      <DropdownMenu.Content>
        <DropdownMenu.LinkItem href="/settings" icon={GearIcon}>
          Settings
        </DropdownMenu.LinkItem>
        <DropdownMenu.LinkItem href="/docs" icon={BookOpenIcon}>
          Documentation
        </DropdownMenu.LinkItem>
        <DropdownMenu.Separator />
        <DropdownMenu.LinkItem
          href="https://developers.cloudflare.com"
          target="_blank"
          icon={ArrowSquareOutIcon}
        >
          Developer Docs
        </DropdownMenu.LinkItem>
      </DropdownMenu.Content>
    </DropdownMenu>
  );
}

长列表

当下拉包含大量条目时,内容会自动将高度限制为可用的视口空间, 并变为可滚动。

import { DropdownMenu, Button } from "@cloudflare/kumo";

/**
 * A dropdown with a very long list of items to demonstrate the max-height
 * behavior — the content scrolls when it exceeds available viewport space.
 */
export function DropdownLongListDemo() {
  const items = Array.from({ length: 30 }, (_, i) => `Option ${i + 1}`);

  return (
    <DropdownMenu>
      <DropdownMenu.Trigger render={<Button>Open long list</Button>} />
      <DropdownMenu.Content>
        {items.map((item) => (
          <DropdownMenu.Item key={item}>{item}</DropdownMenu.Item>
        ))}
      </DropdownMenu.Content>
    </DropdownMenu>
  );
}

API 参考

管理下拉状态的主组件。

PropTypeDefaultDescription
variant"default" | "danger""default"

下拉项的视觉样式。

  • "default" — 标准项目外观
  • "danger" — 带红色文本的破坏性操作

点击即可打开下拉的按钮。

PropTypeDefault

No component-specific props. Accepts standard HTML attributes.

菜单弹出层。它组合了 Portal、Positioner 和 Popup:诸如 side、 align、sideOffset 和 alignOffset 之类的定位属性传递给 Positioner,className 传递给 Popup。

PropTypeDefaultDescription
sideOffsetnumber8

与触发器的像素距离。

containerPortalContainer-

Portal 的容器元素。用于在 Shadow DOM 或自定义容器内渲染。

组成 Content 的各部分。当你需要分别对 Positioner 和 Popup 进行样式 调整或配置时,可直接组合它们。Portal 遵循 KumoPortalProvider, Popup 承载菜单表面样式和打开/关闭动画。

<DropdownMenu.Portal>
  <DropdownMenu.Positioner side="top" align="end">
    <DropdownMenu.Popup className="min-w-56">
      <DropdownMenu.Item>Edit</DropdownMenu.Item>
    </DropdownMenu.Popup>
  </DropdownMenu.Positioner>
</DropdownMenu.Portal>

两者接受与 Base UI 的 Menu.Positioner 和 Menu.Popup 相同的属性。 完整列表请参阅 Base UI Menu 文档。

用于操作的单个菜单条目。

PropTypeDefaultDescription
iconIcon | ReactNode-

标签前显示的图标。

variant"default" | "danger""default"

项目的视觉样式。

selectedboolean-

当为 true 时,显示选中标记指示符。

insetboolean-

添加左侧内边距,以与带图标的项目对齐。

onClick(event: React.MouseEvent) => void-

项目被点击时的回调。

closeOnClickbooleantrue

点击此项后菜单是否关闭。

disabledboolean-

当为 true 时,该项无法交互。

导航到 URL 的菜单条目。渲染语义化的 <a> 元素。导航链接请使用此组件,而不是 Item。

PropTypeDefaultDescription
hrefstring-

点击时要导航到的 URL。

iconIcon | ReactNode-

标签前显示的图标。

variant"default" | "danger""default"

项目的视觉样式。

insetboolean-

添加左侧内边距,以与带图标的项目对齐。

targetstring-

链接目标属性(例如新标签页使用 "_blank")。

renderReactElement | ((props, state) => ReactElement)-

自定义渲染为链接的元素。用于与框架路由器集成(例如 Next.js Link)。

可开启或关闭的菜单条目。适用于独立的布尔选项。

PropTypeDefaultDescription
checkedboolean-

该项是否被选中。

defaultCheckedbooleanfalse

该项初始是否被选中(非受控)。

onCheckedChange(checked: boolean, event: ChangeEventDetails) => void-

选中状态变化时的回调。

closeOnClickbooleanfalse

点击此项后菜单是否关闭。

disabledboolean-

当为 true 时,该项无法交互。

嵌套子菜单的主组件。将 SubTrigger 和 SubContent 包裹在此组件内。

PropTypeDefault

No component-specific props. Accepts standard HTML attributes.

悬停或点击时打开嵌套子菜单的菜单条目。自动显示插入标记(caret)图标, 并在其子菜单打开时保持高亮。与 Item 一样,其 icon 属性接受 Phosphor 图标组件或渲染的元素。

PropTypeDefaultDescription
iconIcon-

标签前显示的图标。

insetboolean-

添加左侧内边距,以与带图标的项目对齐。

子菜单条目的容器。相对于 SubTrigger 定位。当子菜单位于触发器旁边时, 它会取消弹出层的内边距,使第一个条目与触发器对齐,并稍微与父菜单重叠。 如果没有足够的空间而移动到触发器的上方或下方,则使用与顶级菜单相同的 间距。传入 sideOffset 或 alignOffset(一个数字或已解析 side 的函数) 进行覆盖。

PropTypeDefault

No component-specific props. Accepts standard HTML attributes.

菜单条目之间的视觉分隔线。

PropTypeDefault

No component-specific props. Accepts standard HTML attributes.

对单选条目分组以实现单选行为。一次只能选中一个条目。

PropTypeDefaultDescription
valueany-

当前选中单选项目的受控值。

defaultValueany-

初始选中的值(非受控)。

onValueChange(value: any, event: ChangeEventDetails) => void-

选中值发生变化时的回调。

disabledboolean-

当 true 时,组中所有单选项目均被禁用。

行为类似单选按钮的菜单条目。必须在 RadioGroup 内使用。

PropTypeDefaultDescription
value*any-

此单选项目的值。

iconIcon | ReactNode-

标签前显示的图标。

insetboolean-

添加左侧内边距,以与带图标的项目对齐。

closeOnClickbooleanfalse

点击此项后菜单是否关闭。

disabledboolean-

当为 true 时,该项无法交互。

显示 RadioItem 的选中状态。默认显示对勾。

PropTypeDefault

No component-specific props. Accepts standard HTML attributes.