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">jane@example.com</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 指定弹出前的毫秒延迟。

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 打开状态的根组件。

属性类型默认值说明
side"top" | "bottom" | "left" | "right""bottom"Which side of the trigger the popover appears on. - `"top"` — Above the trigger - `"bottom"` — Below the trigger - `"left"` — Left of the trigger - `"right"` — Right of the trigger

Popover.Trigger

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

属性类型默认值

该组件没有专属属性,接受标准 HTML 属性。

Popover.Portal

将 Popover 渲染到应用 DOM 层级之外。默认使用 KumoPortalProvider 提供的容器, 也可通过 container 属性覆盖。

属性类型默认值

该组件没有专属属性,接受标准 HTML 属性。

Popover.Backdrop

渲染在 Popover 后方的可选背景遮罩。

属性类型默认值

该组件没有专属属性,接受标准 HTML 属性。

Popover.Positioner

相对触发器或 anchor 定位弹层。使用 side、align 及其偏移属性控制摆放位置。

属性类型默认值

该组件没有专属属性,接受标准 HTML 属性。

Popover.Popup

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

属性类型默认值

该组件没有专属属性,接受标准 HTML 属性。

Popover.Arrow

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

属性类型默认值

该组件没有专属属性,接受标准 HTML 属性。

Popover.Viewport

当一个弹层被多个触发器共享时,用于为内容切换添加动画的视口。 普通 Popover 或可滚动内容不需要它。

属性类型默认值

该组件没有专属属性,接受标准 HTML 属性。

Popover.Content

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

属性类型默认值

该组件没有专属属性,接受标准 HTML 属性。

Popover.Title

为无障碍标识 Popover 的标题。

属性类型默认值

该组件没有专属属性,接受标准 HTML 属性。

Popover.Description

提供 Popover 内容补充说明的段落。

属性类型默认值

该组件没有专属属性,接受标准 HTML 属性。

Popover.Close

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

属性类型默认值

该组件没有专属属性,接受标准 HTML 属性。