组件
气泡浮层 Popover
在指定触发锚点周围展示富文本内容、轻量表单或操作卡片,支持自动碰撞避让与完整的焦点管理。
基础用法
点击触发按钮即可在就近位置弹出一个浮层卡片:
Loading…
安装与引入
通过 CLI 自动添加组件,或手动复制源码至项目中:
pnpm dlx @wui-design/cli@latest add @wui/popover安装基础依赖与图标库
pnpm add radix-ui lucide-react clsx tailwind-merge复制组件源码到
components/ui/popover.tsx"use client"
import * as React from "react"
import { Popover as PopoverPrimitive } from "radix-ui"
import { cn } from "@/lib/utils"
/** 管理浮层的受控或非受控打开状态。 */
function Popover(props: React.ComponentProps<typeof PopoverPrimitive.Root>) {
return <PopoverPrimitive.Root data-slot="popover" {...props} />
}
/** 将浮层定位到指定容器,默认挂载到 document.body。 */
function PopoverPortal(
props: React.ComponentProps<typeof PopoverPrimitive.Portal>
) {
return <PopoverPrimitive.Portal data-slot="popover-portal" {...props} />
}
/** 触发浮层打开或关闭,通常配合 asChild 复用现有按钮。 */
function PopoverTrigger(
props: React.ComponentProps<typeof PopoverPrimitive.Trigger>
) {
return <PopoverPrimitive.Trigger data-slot="popover-trigger" {...props} />
}
/** 可访问性友好的浮层标题。 */
function PopoverTitle({ className, ...props }: React.ComponentProps<"h2">) {
return (
<h2
data-slot="popover-title"
className={cn("text-sm font-semibold leading-none", className)}
{...props}
/>
)
}
/** 浮层的辅助说明文本。 */
function PopoverDescription({
className,
...props
}: React.ComponentProps<"p">) {
return (
<p
data-slot="popover-description"
className={cn("text-muted-foreground text-sm leading-5", className)}
{...props}
/>
)
}
/** 关闭当前浮层,可通过 asChild 附着到现有控件。 */
function PopoverClose(
props: React.ComponentProps<typeof PopoverPrimitive.Close>
) {
return <PopoverPrimitive.Close data-slot="popover-close" {...props} />
}
export interface PopoverContentProps extends React.ComponentProps<
typeof PopoverPrimitive.Content
> {
/** 浮层与触发器之间的距离,单位为像素。@default 6 */
sideOffset?: number
/** 是否显示指向触发器的箭头。@default false */
showArrow?: boolean
}
/** 承载可交互内容的浮层面板,并自动处理碰撞避让与焦点。 */
function PopoverContent({
className,
align = "center",
sideOffset = 6,
showArrow = false,
children,
...props
}: PopoverContentProps) {
return (
<PopoverPortal>
<PopoverPrimitive.Content
data-slot="popover-content"
align={align}
sideOffset={sideOffset}
className={cn(
"bg-popover text-popover-foreground z-50 w-72 origin-(--radix-popover-content-transform-origin) rounded-lg border p-4 shadow-md outline-none will-change-[transform,opacity] data-[state=open]:animate-in data-[state=open]:fade-in-0 data-[state=open]:zoom-in-95 data-[state=open]:duration-200 data-[state=open]:ease-[cubic-bezier(0.22,1,0.36,1)] data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[state=closed]:duration-150 data-[state=closed]:ease-in data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 motion-reduce:animate-none",
className
)}
{...props}
>
{children}
{showArrow ? (
<PopoverPrimitive.Arrow
data-slot="popover-arrow"
className="fill-popover stroke-border"
width={10}
height={5}
/>
) : null}
</PopoverPrimitive.Content>
</PopoverPortal>
)
}
/** 为嵌套浮层指定定位锚点。 */
function PopoverAnchor(
props: React.ComponentProps<typeof PopoverPrimitive.Anchor>
) {
return <PopoverPrimitive.Anchor data-slot="popover-anchor" {...props} />
}
export {
Popover,
PopoverAnchor,
PopoverClose,
PopoverContent,
PopoverDescription,
PopoverPortal,
PopoverTitle,
PopoverTrigger,
}
属性 Props
Popover (根组件)
| 属性 | 类型 | 默认值 | 说明 |
|---|---|---|---|
| open | boolean | — | 受控模式下的浮层可见状态。 |
| defaultOpen | boolean | false | 非受控模式下的初始打开状态。 |
| onOpenChange | (open: boolean) => void | — | 浮层打开或关闭时触发的回调函数。 |
| modal | boolean | false | 是否将 Popover 渲染为模态(阻止页面其余部分的交互与滚动)。 |
PopoverContent
| 属性 | 类型 | 默认值 | 说明 |
|---|---|---|---|
| side | "top" | "right" | "bottom" | "left" | "bottom" | 浮层优先出现在触发锚点的哪一侧。 |
| align | "start" | "center" | "end" | "center" | 浮层与触发锚点在交叉轴上的对齐方式。 |
| sideOffset | number | 6 | 浮层与触发锚点之间的间距(单位:像素)。 |
| showArrow | boolean | false | 是否渲染指向触发锚点的小三角箭头指示器。 |
| className | string | — | 应用于浮层容器的额外 CSS 类名。 |
PopoverTrigger / PopoverClose
| 属性 | 类型 | 默认值 | 说明 |
|---|---|---|---|
| asChild | boolean | false | 启用后将打开/关闭行为绑定到唯一子元素(如 `<Button>`)。 |
事件 Events
| 属性 | 类型 | 默认值 | 说明 |
|---|---|---|---|
| onOpenChange | (open: boolean) => void | — | 浮层可见性发生变化时调用(点击外部、按下 Esc 键或点击关闭按钮)。 |
| onInteractOutside | (event: CustomEvent) => void | — | 用户在浮层外部发生点击或聚焦时触发,可通过 event.preventDefault() 阻止自动关闭。 |
| onEscapeKeyDown | (event: KeyboardEvent) => void | — | 用户按下键盘 Esc 键时触发。 |
使用场景与设计规范
Popover 用于就地承载比 Tooltip 更加复杂的可交互内容:
- 选型对比:
- Popover vs Tooltip:Tooltip 仅用于鼠标悬停展示纯文本提示,禁止包含可点击链接或输入框;Popover 专为包含表单、多选框、按钮等交互设计。
- Popover vs Dialog:Popover 紧贴触发元素,适合轻量设置与单列筛选;Dialog 居中遮罩,适合大表单、重要确认与复杂流程。
- 避免过重表单:Popover 内部宜保持紧凑(推荐 1~3 个字段),如果需要滚动长表单,请改用 Sheet 侧抽屉或 Dialog 模态弹窗。
- 语义化标题配对:推荐始终在内部放置
PopoverTitle与PopoverDescription,为屏幕阅读器建立清晰的无障碍无缝解读。
场景示例
快捷筛选与多选条件面板
在表格工具栏中展开包含状态多选与重置操作的筛选气泡:
Loading…
轻量配置与表单输入
受控模式下调整画布宽高参数,支持取消与保存:
Loading…
无障碍与交互 Accessibility
- 焦点管理:浮层打开后,焦点会自动安全转移到浮层内的第一个可交互元素;关闭后焦点精确恢复至触发按钮。
- 快捷键:按下 Esc 即可随时安全退出并关闭浮层。
- 视口碰撞翻转:内置自动边界检测(Floating UI / Radix Popper),当屏幕下方空间不足时,自动翻转至上方或侧面,防止内容被浏览器视口截断。
- 方向感知动效:浮层以触发器一侧为缩放原点(
--radix-popover-content-transform-origin)淡入展开并向外轻微滑入,收起时更快地淡出;系统开启“减少动态效果”时自动关闭。