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

export function PopoverHeroDemo() {
  return (
    <Popover.Root>
      <Popover.Trigger
        render={
          <Button shape="square" icon={BellIcon} aria-label="Notifications" />
        }
      />
      <Popover.Portal>
        <Popover.Positioner>
          <Popover.Popup>
            <Popover.Arrow />
            <Popover.Title>Notifications</Popover.Title>
            <Popover.Description>
              You are all caught up. Good job!
            </Popover.Description>
          </Popover.Popup>
        </Popover.Positioner>
      </Popover.Portal>
    </Popover.Root>
  );
}

安装

桶式导出

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

细粒度导入

import { Popover } from "@cloudflare/kumo/components/popover";

用法

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

export default function Example() {
  return (
    <Popover.Root>
      <Popover.Trigger render={<Button />}>Open</Popover.Trigger>
      <Popover.Portal>
        <Popover.Positioner>
          <Popover.Popup>
            <Popover.Arrow />
            <Popover.Title>Popover Title</Popover.Title>
            <Popover.Description>Popover content goes here.</Popover.Description>
          </Popover.Popup>
        </Popover.Positioner>
      </Popover.Portal>
    </Popover.Root>
  );
}

Popover 与 Tooltip 的对比

Popover 虽然可以配置为悬停触发(使用 openOnHover),但它与 Tooltip 的用途不同。理解何时使用两者,对于可访问性 和用户体验都很重要。

TooltipPopover
用途

简短、不可交互的文字标签,用于内容标识

丰富、可交互的内容容器

内容仅限纯文本

任意内容:链接、按钮、表单、图片

触发方式悬停或聚焦点击(默认)或悬停
ARIA 角色

role="tooltip"

aria-haspopup

键盘操作不可聚焦

打开后焦点进入内部并被限制在其中

当你需要给图标按钮加标签或提供简短说明时,使用 Tooltip。 当用户需要与其中的内容交互时——例如点击链接、 填写表单或用按钮关闭——则使用 Popover。

示例

基础 Popover

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

export function PopoverBasicDemo() {
  return (
    <Popover.Root>
      <Popover.Trigger render={<Button />}>Open Popover</Popover.Trigger>
      <Popover.Portal>
        <Popover.Positioner>
          <Popover.Popup>
            <Popover.Arrow />
            <Popover.Title>Popover Title</Popover.Title>
            <Popover.Description>
              This is a basic popover with a title and description.
            </Popover.Description>
          </Popover.Popup>
        </Popover.Positioner>
      </Popover.Portal>
    </Popover.Root>
  );
}

已弃用的 Content 兼容性

Popover.Content 已弃用,但仍保留可用,以确保现有的 Popover 不会失效。它将 Portal、Positioner、Popup 和 Arrow 组合为一个组件,并且仅使用透明度平滑淡入,不带缩放或 变换动画。新的 Popover 应按照基础示例所示 组合各个独立部分。

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

export function PopoverLegacyContentDemo() {
  return (
    <Popover>
      <Popover.Trigger render={<Button variant="secondary" />}>
        Open legacy Content
      </Popover.Trigger>
      <Popover.Content className="w-72">
        <Popover.Title>Compatibility mode</Popover.Title>
        <Popover.Description>
          This legacy wrapper fades in without scaling or transforming.
        </Popover.Description>
      </Popover.Content>
    </Popover>
  );
}

可滚动内容

当箭头和标题需要保持固定时,请将溢出的内容放入 内部滚动容器中。已弃用的 Popover.Content 上现有的溢出样式仍由其仅淡入的过渡所支持。

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

export function PopoverOverflowDemo() {
  return (
    <Popover.Root>
      <Popover.Trigger render={<Button variant="secondary" />}>
        Open scrollable Popover
      </Popover.Trigger>
      <Popover.Portal>
        <Popover.Positioner>
          <Popover.Popup className="w-72">
            <Popover.Arrow />
            <Popover.Title>Recent notifications</Popover.Title>
            <Popover.Description>
              The list scrolls while the heading and arrow stay in place.
            </Popover.Description>
            <div
              aria-label="Recent notifications"
              className="mt-3 max-h-40 overflow-y-auto rounded-md border border-kumo-hairline outline-none focus-visible:ring-2 focus-visible:ring-kumo-brand"
              tabIndex={0}
            >
              {Array.from({ length: 10 }, (_, index) => (
                <div
                  key={index}
                  className="border-b border-kumo-hairline px-3 py-2 last:border-b-0"
                >
                  Notification {index + 1}
                </div>
              ))}
            </div>
          </Popover.Popup>
        </Popover.Positioner>
      </Popover.Portal>
    </Popover.Root>
  );
}

CSS 变量

Base UI 将触发元素与视口边缘之间的空间暴露为 --available-height。此示例为弹出层设置 320px 的首选高度, 然后将该变量用作其最大高度,以便在可用空间较少时 弹出层收缩且其内部内容滚动。

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

export function PopoverCssVariablesDemo() {
  return (
    <Popover.Root>
      <Popover.Trigger render={<Button />}>Open tall Popover</Popover.Trigger>
      <Popover.Portal>
        <Popover.Positioner>
          <Popover.Popup className="h-80 max-h-[var(--available-height)] w-72">
            <Popover.Arrow />
            <Popover.Title>Available-height popup</Popover.Title>
            <Popover.Description>
              The popup is 320px tall unless the viewport provides less space.
            </Popover.Description>
            <div
              aria-label="Popover rows"
              className="mt-3 min-h-0 flex-1 overflow-y-auto rounded-md border border-kumo-hairline outline-none focus-visible:ring-2 focus-visible:ring-kumo-brand"
              tabIndex={0}
            >
              {Array.from({ length: 12 }, (_, index) => (
                <div
                  key={index}
                  className="border-b border-kumo-hairline px-3 py-2 last:border-b-0"
                >
                  Row {index + 1}
                </div>
              ))}
            </div>
          </Popover.Popup>
        </Popover.Positioner>
      </Popover.Portal>
    </Popover.Root>
  );
}

带关闭按钮

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

export function PopoverWithCloseDemo() {
  return (
    <Popover.Root>
      <Popover.Trigger render={<Button />}>Open Settings</Popover.Trigger>
      <Popover.Portal>
        <Popover.Positioner>
          <Popover.Popup>
            <Popover.Arrow />
            <Popover.Title>Settings</Popover.Title>
            <Popover.Description>
              Configure your preferences below.
            </Popover.Description>
            <div className="mt-3">
              <Popover.Close render={<Button variant="secondary" size="sm" />}>
                Close
              </Popover.Close>
            </div>
          </Popover.Popup>
        </Popover.Positioner>
      </Popover.Portal>
    </Popover.Root>
  );
}

定位

在 Popover.Positioner 上使用 side 属性来控制 Popover 相对触发元素出现的位置。

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

export function PopoverPositionDemo() {
  return (
    <div className="flex flex-wrap gap-4">
      <Popover.Root>
        <Popover.Trigger render={<Button variant="secondary" />}>
          Bottom
        </Popover.Trigger>
        <Popover.Portal>
          <Popover.Positioner side="bottom">
            <Popover.Popup>
              <Popover.Arrow />
              <Popover.Title>Bottom</Popover.Title>
              <Popover.Description>
                Popover on bottom (default).
              </Popover.Description>
            </Popover.Popup>
          </Popover.Positioner>
        </Popover.Portal>
      </Popover.Root>

      <Popover.Root>
        <Popover.Trigger render={<Button variant="secondary" />}>
          Top
        </Popover.Trigger>
        <Popover.Portal>
          <Popover.Positioner side="top">
            <Popover.Popup>
              <Popover.Arrow />
              <Popover.Title>Top</Popover.Title>
              <Popover.Description>Popover on top.</Popover.Description>
            </Popover.Popup>
          </Popover.Positioner>
        </Popover.Portal>
      </Popover.Root>

      <Popover.Root>
        <Popover.Trigger render={<Button variant="secondary" />}>
          Left
        </Popover.Trigger>
        <Popover.Portal>
          <Popover.Positioner side="left">
            <Popover.Popup>
              <Popover.Arrow />
              <Popover.Title>Left</Popover.Title>
              <Popover.Description>Popover on left.</Popover.Description>
            </Popover.Popup>
          </Popover.Positioner>
        </Popover.Portal>
      </Popover.Root>

      <Popover.Root>
        <Popover.Trigger render={<Button variant="secondary" />}>
          Right
        </Popover.Trigger>
        <Popover.Portal>
          <Popover.Positioner side="right">
            <Popover.Popup>
              <Popover.Arrow />
              <Popover.Title>Right</Popover.Title>
              <Popover.Description>Popover on right.</Popover.Description>
            </Popover.Popup>
          </Popover.Positioner>
        </Popover.Portal>
      </Popover.Root>
    </div>
  );
}

自定义内容

Popover 可以包含任意内容,包括带有头像、 按钮等的自定义布局。

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

export function PopoverCustomContentDemo() {
  return (
    <Popover.Root>
      <Popover.Trigger render={<Button />}>User Profile</Popover.Trigger>
      <Popover.Portal>
        <Popover.Positioner>
          <Popover.Popup className="w-64">
            <Popover.Arrow />
            <div className="flex items-center gap-3">
              <div className="size-10 rounded-full bg-kumo-recessed" />
              <div>
                <Popover.Title>Jane Doe</Popover.Title>
                <p className="text-sm text-kumo-subtle">[email protected]</p>
              </div>
            </div>
            <div className="mt-3 flex gap-2 border-t border-kumo-hairline pt-3">
              <Button variant="secondary" size="sm" className="flex-1">
                Profile
              </Button>
              <Popover.Close
                render={<Button variant="ghost" size="sm" className="flex-1" />}
              >
                Sign Out
              </Popover.Close>
            </div>
          </Popover.Popup>
        </Popover.Positioner>
      </Popover.Portal>
    </Popover.Root>
  );
}

悬停打开

在触发器上使用 openOnHover,即可在用户悬停时打开 Popover。 还可以通过 delay 指定 Popover 出现前的毫秒延迟。

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

export function PopoverOpenOnHoverDemo() {
  return (
    <Popover.Root>
      <Popover.Trigger
        openOnHover
        delay={200}
        render={<Button variant="secondary" />}
      >
        Hover Me
      </Popover.Trigger>
      <Popover.Portal>
        <Popover.Positioner>
          <Popover.Popup>
            <Popover.Arrow />
            <Popover.Title>Hover Triggered</Popover.Title>
            <Popover.Description>
              This popover opens on hover with a 200ms delay. It can still
              contain interactive content like buttons and links.
            </Popover.Description>
            <div className="mt-3">
              <Popover.Close render={<Button variant="secondary" size="sm" />}>
                Got it
              </Popover.Close>
            </div>
          </Popover.Popup>
        </Popover.Positioner>
      </Popover.Portal>
    </Popover.Root>
  );
}

虚拟锚点

在 Popover.Positioner 上使用 anchor 属性,将 Popover 定位到触发元素以外的其他 元素上,或定位到某个虚拟点(例如 getBoundingClientRect() 返回的 DOMRect)。当触发器与期望的锚点位于不同的组件树中时, 这会非常有用。

NameStatus
api-gatewayActive
auth-serviceActive
worker-prodPaused
import { useState, useRef } from "react";
import { Popover, Button } from "@cloudflare/kumo";
import { DotsThree } from "@phosphor-icons/react";

/** Popover anchored to a virtual element instead of a trigger. */
export function PopoverVirtualAnchorDemo() {
  const [selectedRow, setSelectedRow] = useState<string | null>(null);
  const [anchorRect, setAnchorRect] = useState<DOMRect | null>(null);
  const rowRefs = useRef<Map<string, HTMLTableRowElement>>(new Map());

  const rows = [
    { id: "1", name: "api-gateway", status: "Active" },
    { id: "2", name: "auth-service", status: "Active" },
    { id: "3", name: "worker-prod", status: "Paused" },
  ];

  const handleEdit = (id: string) => {
    const row = rowRefs.current.get(id);
    if (row) {
      setAnchorRect(row.getBoundingClientRect());
      setSelectedRow(id);
    }
  };

  return (
    <div className="w-full">
      <div className="overflow-hidden rounded-lg border border-kumo-hairline">
        <table className="w-full text-sm">
          <thead className="bg-kumo-elevated">
            <tr>
              <th className="px-4 py-2 text-left font-medium">Name</th>
              <th className="px-4 py-2 text-left font-medium">Status</th>
              <th className="w-12 px-4 py-2"></th>
            </tr>
          </thead>
          <tbody className="divide-y divide-kumo-hairline">
            {rows.map((row) => (
              <tr
                key={row.id}
                ref={(el) => {
                  if (el) rowRefs.current.set(row.id, el);
                }}
                className={
                  selectedRow === row.id ? "bg-kumo-recessed" : "bg-kumo-base"
                }
              >
                <td className="px-4 py-2 font-mono">{row.name}</td>
                <td className="px-4 py-2 text-kumo-subtle">{row.status}</td>
                <td className="px-4 py-2">
                  <Button
                    size="xs"
                    variant="ghost"
                    shape="square"
                    icon={DotsThree}
                    aria-label={`Actions for ${row.name}`}
                    onClick={() => handleEdit(row.id)}
                  />
                </td>
              </tr>
            ))}
          </tbody>
        </table>
      </div>
      <Popover.Root
        open={!!selectedRow}
        onOpenChange={(open) => !open && setSelectedRow(null)}
      >
        <Popover.Portal>
          <Popover.Positioner
            side="left"
            anchor={
              anchorRect
                ? { getBoundingClientRect: () => anchorRect }
                : undefined
            }
          >
            <Popover.Popup>
              <Popover.Arrow />
              <Popover.Title>
                Edit {rows.find((r) => r.id === selectedRow)?.name}
              </Popover.Title>
              <Popover.Description>
                The popover anchors to the selected row, not the icon button.
              </Popover.Description>
              <div className="mt-3">
                <Popover.Close
                  render={<Button size="sm" variant="secondary" />}
                >
                  Close
                </Popover.Close>
              </div>
            </Popover.Popup>
          </Popover.Positioner>
        </Popover.Portal>
      </Popover.Root>
    </div>
  );
}

API 参考

Popover.Root

负责管理 Popover 打开状态的基础组件。

PropTypeDefaultDescription
side"top" | "bottom" | "left" | "right""bottom"

弹出层出现在触发器的哪一侧。

  • "top" — 触发器上方
  • "bottom" — 触发器下方
  • "left" — 触发器左侧
  • "right" — 触发器右侧

Popover.Trigger

点击时打开 Popover 的按钮。使用 render 渲染你自己的元素。

PropTypeDefault

No component-specific props. Accepts standard HTML attributes.

Popover.Portal

在应用的 DOM 层级之外渲染 Popover。它默认使用来自 KumoPortalProvider 的容器,并接受 container 属性 来覆盖该容器。

PropTypeDefault

No component-specific props. Accepts standard HTML attributes.

Popover.Backdrop

一个可选背景遮罩,渲染在 Popover 后方。

PropTypeDefault

No component-specific props. Accepts standard HTML attributes.

Popover.Positioner

将弹出层相对于其触发元素或某个 anchor 定位。使用 side、 align 及其偏移属性来控制放置位置。

PropTypeDefault

No component-specific props. Accepts standard HTML attributes.

Popover.Popup

带样式的弹出层容器。将 Popover.Arrow 和 Popover 内容 直接放在其中。

PropTypeDefault

No component-specific props. Accepts standard HTML attributes.

Popover.Arrow

一个可选箭头,指向 Popover 的锚点。

PropTypeDefault

No component-specific props. Accepts standard HTML attributes.

Popover.Viewport

用于在多个触发元素共用一个弹出层时,为内容变化添加动画的视口。 普通的 Popover 或可滚动内容并不需要它。

PropTypeDefault

No component-specific props. Accepts standard HTML attributes.

Popover.Content

一个已弃用的兼容包装组件,组合了 Portal、Positioner、 Popup 和 Arrow。现有用法仍受支持,但新代码应 组合上文所示的各个独立部分。

PropTypeDefault

No component-specific props. Accepts standard HTML attributes.

Popover.Title

为 Popover 提供无障碍标签的标题。

PropTypeDefault

No component-specific props. Accepts standard HTML attributes.

Popover.Description

提供关于 Popover 内容补充信息的段落。

PropTypeDefault

No component-specific props. Accepts standard HTML attributes.

Popover.Close

点击时关闭 Popover 的按钮。使用 render 渲染你自己的元素。

PropTypeDefault

No component-specific props. Accepts standard HTML attributes.