wui
组件

气泡浮层 Popover

在指定触发锚点周围展示富文本内容、轻量表单或操作卡片,支持自动碰撞避让与完整的焦点管理。

第三方依赖 · radix-ui

基础用法

点击触发按钮即可在就近位置弹出一个浮层卡片:

Loading…

安装与引入

通过 CLI 自动添加组件,或手动复制源码至项目中:

pnpm dlx @wui-design/cli@latest add @wui/popover
安装基础依赖与图标库
pnpm add radix-ui lucide-react clsx tailwind-merge
复制组件源码到 components/ui/popover.tsx
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 (根组件)

属性类型默认值说明
openboolean—受控模式下的浮层可见状态。
defaultOpenbooleanfalse非受控模式下的初始打开状态。
onOpenChange(open: boolean) => void—浮层打开或关闭时触发的回调函数。
modalbooleanfalse是否将 Popover 渲染为模态(阻止页面其余部分的交互与滚动)。

PopoverContent

属性类型默认值说明
side"top" | "right" | "bottom" | "left""bottom"浮层优先出现在触发锚点的哪一侧。
align"start" | "center" | "end""center"浮层与触发锚点在交叉轴上的对齐方式。
sideOffsetnumber6浮层与触发锚点之间的间距(单位:像素)。
showArrowbooleanfalse是否渲染指向触发锚点的小三角箭头指示器。
classNamestring—应用于浮层容器的额外 CSS 类名。

PopoverTrigger / PopoverClose

属性类型默认值说明
asChildbooleanfalse启用后将打开/关闭行为绑定到唯一子元素(如 `<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)淡入展开并向外轻微滑入,收起时更快地淡出;系统开启“减少动态效果”时自动关闭。