Popover 气泡卡片
@lark-ui/core
import { Popover, Button } from "@lark-ui/core";
import { BellIcon } from "@phosphor-icons/react";

export function PopoverHeroDemo() {
  return (
    <Popover.Root>
      <Popover.Trigger
        render={<Button shape="square" icon={BellIcon} aria-label="通知" />}
      />
      <Popover.Portal>
        <Popover.Positioner>
          <Popover.Popup>
            <Popover.Arrow />
            <Popover.Title>通知</Popover.Title>
            <Popover.Description>你已全部看完,干得漂亮!</Popover.Description>
          </Popover.Popup>
        </Popover.Positioner>
      </Popover.Portal>
    </Popover.Root>
  );
}

安装

统一导出

import { Popover } from "@lark-ui/core";

按需导出

import { Popover } from "@lark-ui/core/components/popover";

用法

import { Popover, Button } from "@lark-ui/core";

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>
  );
}

气泡卡片与工具提示

虽然气泡卡片也可以通过悬停触发(使用 openOnHover),但它的用途与工具提示不同。 了解两者各自的适用场景,对无障碍和用户体验都很重要。

工具提示气泡卡片
用途

用于标识的简短、非交互式文本标签

富内容、可交互的内容容器

内容仅纯文本

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

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

role="tooltip"

aria-haspopup

键盘不可聚焦

打开时焦点移入内部并被锁定

使用工具提示(Tooltip)当你需要为图标按钮添加标签或提供简短说明时。 使用气泡卡片(Popover)当用户需要与内部内容交互时,例如点击链接、填写表单,或通过按钮关闭。

示例

基础气泡卡片

import { Popover, Button } from "@lark-ui/core";

export function PopoverBasicDemo() {
  return (
    <Popover.Root>
      <Popover.Trigger render={<Button />}>打开气泡卡片</Popover.Trigger>
      <Popover.Portal>
        <Popover.Positioner>
          <Popover.Popup>
            <Popover.Arrow />
            <Popover.Title>气泡卡片标题</Popover.Title>
            <Popover.Description>
              这是一个包含标题和描述的基础气泡卡片。
            </Popover.Description>
          </Popover.Popup>
        </Popover.Positioner>
      </Popover.Portal>
    </Popover.Root>
  );
}

已弃用的 Popover.Content 兼容性

Popover.Content 已被弃用,但仍然保留,以免现有气泡卡片失效。它把 Portal、Positioner、 Popup 和 Arrow 合并为一个组件,并且仅通过不透明度实现平滑淡入,不带缩放或位移动画。 新的气泡卡片应像基础示例中那样组合各个独立部分。

import { Popover, Button } from "@lark-ui/core";

export function PopoverLegacyContentDemo() {
  return (
    <Popover>
      <Popover.Trigger render={<Button variant="secondary" />}>
        打开旧版 Content
      </Popover.Trigger>
      <Popover.Content className="w-72">
        <Popover.Title>兼容模式</Popover.Title>
        <Popover.Description>
          这个旧版包装器只做淡入,不做缩放或变换。
        </Popover.Description>
      </Popover.Content>
    </Popover>
  );
}

可滚动内容

当箭头和标题需要保持固定时,请把溢出的内容放在内部滚动容器中。已弃用的 Popover.Content 上现有的溢出样式仍然受支持,因为它只使用淡入淡出过渡。

import { Popover, Button } from "@lark-ui/core";

export function PopoverOverflowDemo() {
  return (
    <Popover.Root>
      <Popover.Trigger render={<Button variant="secondary" />}>
        打开可滚动的气泡卡片
      </Popover.Trigger>
      <Popover.Portal>
        <Popover.Positioner>
          <Popover.Popup className="w-72">
            <Popover.Arrow />
            <Popover.Title>最近通知</Popover.Title>
            <Popover.Description>
              列表可滚动,标题和箭头保持固定。
            </Popover.Description>
            <div
              aria-label="最近通知"
              className="mt-3 max-h-40 overflow-y-auto rounded-md border border-lark-hairline outline-none focus-visible:ring-2 focus-visible:ring-lark-brand"
              tabIndex={0}
            >
              {Array.from({ length: 10 }, (_, index) => (
                <div
                  key={index}
                  className="border-b border-lark-hairline px-3 py-2 last:border-b-0"
                >
                  通知 {index + 1}
                </div>
              ))}
            </div>
          </Popover.Popup>
        </Popover.Positioner>
      </Popover.Portal>
    </Popover.Root>
  );
}

CSS 变量

Base UI 通过 --available-height 暴露触发元素与视口边缘之间的可用空间。此示例先把弹层的 首选高度设为 320px,再将该变量用作最大高度,这样空间不足时弹层会收缩,内部内容随之滚动。

import { Popover, Button } from "@lark-ui/core";

export function PopoverCssVariablesDemo() {
  return (
    <Popover.Root>
      <Popover.Trigger render={<Button />}>打开较高的气泡卡片</Popover.Trigger>
      <Popover.Portal>
        <Popover.Positioner>
          <Popover.Popup className="h-80 max-h-[var(--available-height)] w-72">
            <Popover.Arrow />
            <Popover.Title>自适应高度的浮层</Popover.Title>
            <Popover.Description>
              除非视口空间不足,否则该浮层高度为 320px。
            </Popover.Description>
            <div
              aria-label="气泡卡片行"
              className="mt-3 min-h-0 flex-1 overflow-y-auto rounded-md border border-lark-hairline outline-none focus-visible:ring-2 focus-visible:ring-lark-brand"
              tabIndex={0}
            >
              {Array.from({ length: 12 }, (_, index) => (
                <div
                  key={index}
                  className="border-b border-lark-hairline px-3 py-2 last:border-b-0"
                >
                  第 {index + 1} 行
                </div>
              ))}
            </div>
          </Popover.Popup>
        </Popover.Positioner>
      </Popover.Portal>
    </Popover.Root>
  );
}

带关闭按钮

import { Popover, Button } from "@lark-ui/core";

export function PopoverWithCloseDemo() {
  return (
    <Popover.Root>
      <Popover.Trigger render={<Button />}>打开设置</Popover.Trigger>
      <Popover.Portal>
        <Popover.Positioner>
          <Popover.Popup>
            <Popover.Arrow />
            <Popover.Title>设置</Popover.Title>
            <Popover.Description>在下方配置你的偏好设置。</Popover.Description>
            <div className="mt-3">
              <Popover.Close render={<Button variant="secondary" size="sm" />}>
                关闭
              </Popover.Close>
            </div>
          </Popover.Popup>
        </Popover.Positioner>
      </Popover.Portal>
    </Popover.Root>
  );
}

定位

在 Popover.Positioner 上使用 side 属性,可以控制气泡卡片相对于触发元素出现的位置。

import { Popover, Button } from "@lark-ui/core";

export function PopoverPositionDemo() {
  return (
    <div className="flex flex-wrap gap-4">
      <Popover.Root>
        <Popover.Trigger render={<Button variant="secondary" />}>
          底部
        </Popover.Trigger>
        <Popover.Portal>
          <Popover.Positioner side="bottom">
            <Popover.Popup>
              <Popover.Arrow />
              <Popover.Title>底部</Popover.Title>
              <Popover.Description>
                气泡卡片显示在下方(默认)。
              </Popover.Description>
            </Popover.Popup>
          </Popover.Positioner>
        </Popover.Portal>
      </Popover.Root>

      <Popover.Root>
        <Popover.Trigger render={<Button variant="secondary" />}>
          顶部
        </Popover.Trigger>
        <Popover.Portal>
          <Popover.Positioner side="top">
            <Popover.Popup>
              <Popover.Arrow />
              <Popover.Title>顶部</Popover.Title>
              <Popover.Description>气泡卡片显示在上方。</Popover.Description>
            </Popover.Popup>
          </Popover.Positioner>
        </Popover.Portal>
      </Popover.Root>

      <Popover.Root>
        <Popover.Trigger render={<Button variant="secondary" />}>
          左侧
        </Popover.Trigger>
        <Popover.Portal>
          <Popover.Positioner side="left">
            <Popover.Popup>
              <Popover.Arrow />
              <Popover.Title>左侧</Popover.Title>
              <Popover.Description>气泡卡片显示在左侧。</Popover.Description>
            </Popover.Popup>
          </Popover.Positioner>
        </Popover.Portal>
      </Popover.Root>

      <Popover.Root>
        <Popover.Trigger render={<Button variant="secondary" />}>
          右侧
        </Popover.Trigger>
        <Popover.Portal>
          <Popover.Positioner side="right">
            <Popover.Popup>
              <Popover.Arrow />
              <Popover.Title>右侧</Popover.Title>
              <Popover.Description>气泡卡片显示在右侧。</Popover.Description>
            </Popover.Popup>
          </Popover.Positioner>
        </Popover.Portal>
      </Popover.Root>
    </div>
  );
}

自定义内容

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

import { Popover, Button } from "@lark-ui/core";

export function PopoverCustomContentDemo() {
  return (
    <Popover.Root>
      <Popover.Trigger render={<Button />}>用户资料</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-lark-recessed" />
              <div>
                <Popover.Title>简·多伊</Popover.Title>
                <p className="text-sm text-lark-subtle">jane@example.com</p>
              </div>
            </div>
            <div className="mt-3 flex gap-2 border-t border-lark-hairline pt-3">
              <Button variant="secondary" size="sm" className="flex-1">
                个人资料
              </Button>
              <Popover.Close
                render={<Button variant="ghost" size="sm" className="flex-1" />}
              >
                退出登录
              </Popover.Close>
            </div>
          </Popover.Popup>
        </Popover.Positioner>
      </Popover.Portal>
    </Popover.Root>
  );
}

悬停打开

在触发元素上使用 openOnHover,可以在用户悬停时打开气泡卡片。你还可以用 delay 指定 气泡卡片出现前的延迟毫秒数。

import { Popover, Button } from "@lark-ui/core";

export function PopoverOpenOnHoverDemo() {
  return (
    <Popover.Root>
      <Popover.Trigger
        openOnHover
        delay={200}
        render={<Button variant="secondary" />}
      >
        悬停试试
      </Popover.Trigger>
      <Popover.Portal>
        <Popover.Positioner>
          <Popover.Popup>
            <Popover.Arrow />
            <Popover.Title>悬停触发</Popover.Title>
            <Popover.Description>
              该气泡卡片在悬停时打开,延迟 200
              毫秒。内部仍可包含按钮、链接等可交互内容。
            </Popover.Description>
            <div className="mt-3">
              <Popover.Close render={<Button variant="secondary" size="sm" />}>
                知道了
              </Popover.Close>
            </div>
          </Popover.Popup>
        </Popover.Positioner>
      </Popover.Portal>
    </Popover.Root>
  );
}

虚拟锚点

在 Popover.Positioner 上使用 anchor 属性,可以让气泡卡片相对触发元素之外的元素定位, 或相对一个虚拟点(例如 getBoundingClientRect() 返回的 DOMRect)定位。当触发元素和期望的 锚点位于不同的组件树中时,这会非常有用。

名称状态
api-gateway运行中
auth-service运行中
worker-prod已暂停
import { useState, useRef } from "react";
import { Popover, Button } from "@lark-ui/core";
import { DotsThree } from "@phosphor-icons/react";

/** 锚定到虚拟元素(而非触发器)的 Popover。 */
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: "运行中" },
    { id: "2", name: "auth-service", status: "运行中" },
    { id: "3", name: "worker-prod", status: "已暂停" },
  ];

  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-lark-hairline">
        <table className="w-full text-sm">
          <thead className="bg-lark-elevated">
            <tr>
              <th className="px-4 py-2 text-left font-medium">名称</th>
              <th className="px-4 py-2 text-left font-medium">状态</th>
              <th className="w-12 px-4 py-2"></th>
            </tr>
          </thead>
          <tbody className="divide-y divide-lark-hairline">
            {rows.map((row) => (
              <tr
                key={row.id}
                ref={(el) => {
                  if (el) rowRefs.current.set(row.id, el);
                }}
                className={
                  selectedRow === row.id ? "bg-lark-recessed" : "bg-lark-base"
                }
              >
                <td className="px-4 py-2 font-mono">{row.name}</td>
                <td className="px-4 py-2 text-lark-subtle">{row.status}</td>
                <td className="px-4 py-2">
                  <Button
                    size="xs"
                    variant="ghost"
                    shape="square"
                    icon={DotsThree}
                    aria-label={`${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>
                编辑 {rows.find((r) => r.id === selectedRow)?.name}
              </Popover.Title>
              <Popover.Description>
                气泡卡片以选中的行为锚点,而不是图标按钮。
              </Popover.Description>
              <div className="mt-3">
                <Popover.Close
                  render={<Button size="sm" variant="secondary" />}
                >
                  关闭
                </Popover.Close>
              </div>
            </Popover.Popup>
          </Popover.Positioner>
        </Popover.Portal>
      </Popover.Root>
    </div>
  );
}

API 参考

Popover.Root

管理气泡卡片打开状态的根组件。

属性类型默认值说明
side"top" | "bottom" | "left" | "right""bottom"气泡卡片显示在触发元素的哪一侧。 - `"top"` — 触发元素上方 - `"bottom"` — 触发元素下方 - `"left"` — 触发元素左侧 - `"right"` — 触发元素右侧

Popover.Trigger

点击时打开气泡卡片的按钮。使用 render 可渲染你自己的元素。

属性类型默认值

该组件没有专有属性,支持标准 HTML 属性。

Popover.Portal

在应用的 DOM 层级之外渲染气泡卡片。默认使用 LarkPortalProvider 提供的容器,也可以通过 container 属性覆盖。

属性类型默认值

该组件没有专有属性,支持标准 HTML 属性。

Popover.Backdrop

渲染在气泡卡片后方的可选遮罩层。

属性类型默认值

该组件没有专有属性,支持标准 HTML 属性。

Popover.Positioner

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

属性类型默认值

该组件没有专有属性,支持标准 HTML 属性。

Popover.Popup

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

属性类型默认值

该组件没有专有属性,支持标准 HTML 属性。

Popover.Arrow

指向气泡卡片锚点的可选箭头。

属性类型默认值

该组件没有专有属性,支持标准 HTML 属性。

Popover.Viewport

当同一个弹层由多个触发元素共享时,用于为内容切换添加动画的视口。普通气泡卡片或滚动内容 不需要它。

属性类型默认值

该组件没有专有属性,支持标准 HTML 属性。

Popover.Content

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

属性类型默认值

该组件没有专有属性,支持标准 HTML 属性。

Popover.Title

为气泡卡片提供标签的标题,用于无障碍。

属性类型默认值

该组件没有专有属性,支持标准 HTML 属性。

Popover.Description

为气泡卡片内容提供补充说明的段落。

属性类型默认值

该组件没有专有属性,支持标准 HTML 属性。

Popover.Close

点击时关闭气泡卡片的按钮。使用 render 可渲染你自己的元素。

属性类型默认值

该组件没有专有属性,支持标准 HTML 属性。