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
不同。了解何时该用哪一种,对无障碍和用户体验都很重要。
| Tooltip | Popover | |
|---|---|---|
| 用途 | 简短、不可交互的文本标签,用于标识 | 富交互的内容容器 |
| 内容 | 仅纯文本 | 任意内容:链接、按钮、表单、图片 |
| 触发方式 | 悬停或聚焦 | 点击(默认)或悬停 |
| ARIA 角色 |
|
|
| 键盘 | 不可聚焦 | 焦点移入内部,打开时锁定在弹层内 |
当需要为图标按钮添加标签或作简短说明时,使用 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)。
当触发器和期望的锚点位于不同的组件树时,这很有用。
| Name | Status | |
|---|---|---|
| api-gateway | Active | |
| auth-service | Active | |
| worker-prod | Paused |
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 属性。