@atlaskit/top-layer

中文文档
7.1.5发布于 2026/11/06Apache-2.0TypeScriptESM + CJS近 30 天下载 44.2万

Low-level layering primitives based on popover and dialog

原始使用文档
文档目录10 节
  1. Popover
  2. Dialog
  3. Page surfaces
  4. Animations
  5. Popover animation
  6. Dialog animation
  7. Progressive enhancement
  8. Accessibility
  9. Known limitations
  10. Documentation

@atlaskit/top-layer

Low-level top-layer primitives using the native Popover API (popover="auto") and <dialog> element.

Use a Design System component first, such as Popup, Modal dialog, Tooltip or Dropdown menu. Use these primitives only if no Design System component fits, behind the platform-dst-top-layer feature flag. Do not use a native popover or <dialog> element directly: the @atlaskit/design-system/no-native-popover-or-dialog ESLint rule blocks it.

Popover

The primary primitive: a <div> with the popover attribute, plus a small lifecycle (animations, role-based focus management, light-dismiss, nested-focus restoration). It does not know about placement or size — compose it with the useAnchoredPopover hook when you need anchor-positioned content.

import { useRef, useState } from 'react';
import { getAriaForTrigger } from '@atlaskit/top-layer/get-aria-for-trigger';
import { Popover } from '@atlaskit/top-layer/popover/popover';
import { PopoverSurface } from '@atlaskit/top-layer/popover-surface';
import { usePopoverId } from '@atlaskit/top-layer/use-popover-id';
import { useAnchoredPopover } from '@atlaskit/top-layer/use-anchored-popover';

function MyPopup() {
    const [isOpen, setIsOpen] = useState(false);
    const triggerRef = useRef<HTMLButtonElement>(null);
    const popoverRef = useRef<HTMLDivElement>(null);
    const popoverId = usePopoverId();

    useAnchoredPopover({
        anchorRef: triggerRef,
        popoverRef,
        placement: { axis: 'block', edge: 'end', align: 'start' },
        isOpen,
    });

    return (
        <>
            <button
                ref={triggerRef}
                onClick={() => setIsOpen((value) => !value)}
                {...getAriaForTrigger({ role: 'dialog', isOpen, popoverId })}
            >
                Open
            </button>
            <Popover
                ref={popoverRef}
                id={popoverId}
                role="dialog"
                label="Settings"
                isOpen={isOpen}
                shouldAnimate
                onClose={() => setIsOpen(false)}
            >
                <PopoverSurface>Content here</PopoverSurface>
            </Popover>
        </>
    );
}
  • Focus restoration is automatic. The browser handles it for outermost popovers; Popover snapshots document.activeElement on open and restores it on close for nested cases.
  • For default overlay styling (background, shadow, border-radius), wrap content in PopoverSurface from @atlaskit/top-layer/popover-surface.
  • For trigger-less or custom-positioned UI (flag, tooltip, react-select menu portal), use Popover on its own without useAnchoredPopover, or pass isEnabled: false to turn positioning off.
  • useAnchoredPopover also owns anchor-relative sizing via inlineSize / blockSize ('content' | 'match-anchor' | 'min-anchor' | 'max-available', both defaulting to 'content'). A viewport cap is written on both axes whatever you pass, and only a placement-axis minimum can beat it. A 'content' inline axis is the content's natural width, so a popover too wide for the space beside its anchor slides to a roomier side rather than wrapping; shouldPreserveInlineSize opts an element you own and have styled out of that write. See the hook's JSDoc and notes/decisions/fit-available-space.md.

Dialog

A modal dialog on the native <dialog> element. isOpen calls showModal() and close(). It has no visual opinions (no width, surface or layout); consumers such as @atlaskit/modal-dialog add their own. A label or labelledBy is required.

import { Dialog } from '@atlaskit/top-layer/dialog-content';

<Dialog isOpen={isOpen} onClose={() => setIsOpen(false)} labelledBy={titleId}>
    <h2 id={titleId}>Heading</h2>
    Content
</Dialog>;
  • onClose({ reason }) fires after an allowed Escape ('escape') or backdrop click ('overlay-click'). Set isOpen={false} in response.
  • dismissedBy limits user dismissal: 'escape-and-outside-click' (default), 'escape' or 'none'. To allow only backdrop clicks, use the default and ignore onClose calls with reason: 'escape'.
  • The <dialog> has closedby="none": Dialog handles Escape and backdrop clicks itself. Other close requests (for example Android back) do not close the dialog and do not call onClose.

Page surfaces

Utilities for page surfaces: surfaces that are not in the top layer (for example a side panel, a grid or the side nav) and must work alongside top-layer surfaces. They replace the @atlaskit/layering open layer observer.

import { hasOpenDialogOrPopover } from '@atlaskit/top-layer/page-surface/has-open-dialog-or-popover';
import { requestClosePopovers } from '@atlaskit/top-layer/page-surface/request-close-popovers';
import { useCloseOnUnclaimedEscape } from '@atlaskit/top-layer/page-surface/use-close-on-unclaimed-escape';

// A side panel that closes on Escape, unless an open dialog or popover claimed it.
useCloseOnUnclaimedEscape({ isEnabled: isOpen, onClose: () => setIsOpen(false) });

// A grid that ignores keys while a dialog or popover opened from it is open.
if (hasOpenDialogOrPopover({ within: event.currentTarget })) {
    return;
}
  • useCloseOnUnclaimedEscape({ isEnabled, onClose, shouldClose, targetRef }) closes a page surface (one that is not in the top layer, for example a side panel) on Escape. The browser closes top-layer surfaces on Escape itself, but a page surface also receives that Escape. The hook ignores a claimed Escape: when event.defaultPrevented is true, when event.repeat is true, when another Escape owner is above the surface (an open layer that closes on Escape, or a marked page surface; see the Escape stack in notes/decisions/manual-light-dismiss-escape-stacking.md), or when shouldClose returns false. Pass surfaceRef to name the surface element: a layer around it is below it. It listens on document, or on targetRef when you pass one (then only while focus is inside it). The targetRef element must be mounted when isEnabled becomes true. Do not use the hook for a Popover or a Dialog: they already close on Escape.
  • hasOpenDialogOrPopover({ within, type }) returns true when an open <dialog>, or an open Popover that has an onClose, is inside within (default: document). Tooltips and popovers without onClose never count. type: 'modal' counts only modal dialogs. Call it at event time: a keydown listener still sees a popover that the same Escape will close.
  • requestClosePopovers({ within }) calls onClose({ reason: 'programmatic' }) on each open closable popover inside within. It never hides anything itself, and it does not ask dialogs. Use it when the layout changes, for example when a side nav collapses.
  • Top-layer surfaces render inline, so a surface opened from inside an element is a DOM descendant of that element. Use within where you used observer namespaces.

Animations

Both Popover and Dialog support CSS-based entry/exit animations via the shouldAnimate prop. Animations use @starting-style for entry and allow-discrete on display/overlay for exit — no JavaScript animation coordination required.

Popover animation

Pass shouldAnimate to enable the default popover entry and exit animation:

import { Popover } from '@atlaskit/top-layer/popover/popover';

<Popover shouldAnimate role="dialog" label="My popover" isOpen={isOpen} onClose={onClose}>
    Content
</Popover>;

For overlay styling (background, shadow, border-radius), wrap children in PopoverSurface from @atlaskit/top-layer/popover-surface.

Use enteringAnimationXcss and exitingAnimationXcss when a composed component needs to override the phase-specific animation styles.

Dialog animation

Pass shouldAnimate to enable the default dialog entry and exit animation:

import { Dialog } from '@atlaskit/top-layer/dialog-content';

<Dialog shouldAnimate isOpen={isOpen} onClose={onClose} label="My dialog">
    Content
</Dialog>;

Use enteringAnimationXcss and exitingAnimationXcss when a composed component needs to override the phase-specific animation styles.

Progressive enhancement

Browsers without @starting-style support will show/hide elements instantly — no broken state. The prefers-reduced-motion: reduce media query sets all transition durations to 0s.

Accessibility

Browser tests in __tests__/playwright/ verify the following WCAG 2.2 success criteria. See notes/goals/accessibility-criteria.md and notes/decisions/accessibility-audit-report.md for details.

A11y criterionBrowser test
1.3.1 Info and Relationships✓
1.3.2 Meaningful Sequence✓
2.1.1 Keyboard✓
2.1.2 No Keyboard Trap✓
2.4.3 Focus Order✓
2.4.7 Focus Visible✓
2.4.11 Focus Not Obscured✓
3.2.1 On Focus✓
4.1.2 Name, Role, Value✓
4.1.3 Status Messages✓
Background inertness (modal dialogs)✓
Close reasons (Dialog)✓

Known limitations

  • Safari <dialog> flex-collapse. On WebKit, a max-height: 100% flex column with a flex: 1 1 auto; overflow: auto child placed directly inside a Dialog collapses to 0px (the block-axis fit-content user-agent default is an indefinite height). Popover sets height: auto to avoid this; Dialog deliberately does not, to keep the primitive free of layout opinions (see notes/decisions/safari-popover-flex-collapse.md). A consumer that builds this shape inside a Dialog should give the column a definite height or apply its own height: auto reset on the surface.
    • TODO: expand this into full public consumer documentation (the recommended workaround and a worked example) before the top-layer Dialog is broadly adopted.

Documentation

See notes/ for project documentation, architecture decisions, migration records, and audit reports. The notes/README.md provides a full index, including current migration status and links to notes/decisions/migration-roadmap.md.

版本与发布记录

@atlaskit/top-layer 共发布 97 个版本,首次发布于 2026/03/25;过去 12 个月发布了 97 个版本。

版本发布日期距今
7.1.5latest2026/11/061 天前
7.1.42026/11/061 天前
7.1.32026/11/043 天前
7.1.22026/11/043 天前
7.1.12026/11/025 天前
7.1.02026/10/318 天前

关于 @atlaskit/top-layer 的常见问题

@atlaskit/top-layer 是什么?

Low-level layering primitives based on popover and dialog

如何安装 @atlaskit/top-layer?

在项目目录运行 npm install @atlaskit/top-layer,或使用 pnpm add / yarn add / bun add。它还需要宿主项目提供 peer 依赖:react(^18.2.0 || ^19.0.0)、react-dom(^18.2.0 || ^19.0.0)。npm 7 及以上与 pnpm 8 及以上默认会自动安装必需的 peer 依赖,Yarn 需要手动安装。

@atlaskit/top-layer 支持 TypeScript 吗?

支持。包内自带 TypeScript 类型声明(package.json 声明了 types 入口),安装后即可获得类型提示,无需再安装 @types 包。

@atlaskit/top-layer 支持 ESM 还是 CommonJS?

同时提供 ES Module 与 CommonJS 入口,import 和 require() 都可以使用,打包工具会自动选择合适的版本。

@atlaskit/top-layer 可以商用吗?

@atlaskit/top-layer 使用 Apache-2.0 许可。宽松许可:允许商业使用、修改和再分发,并附带明确的专利授权;分发时需保留许可与 NOTICE 文件,修改过的文件要注明改动。以上为概括,不构成法律意见,请以许可原文为准。

@atlaskit/top-layer 的下载量有多少?

根据 npm 官方下载统计,@atlaskit/top-layer 在 2026/10/07 – 2026/11/05 这 30 天内被下载 442,003 次,日均约 14,733 次。下载量会把 CI 构建、镜像同步和作为其他包依赖的安装一并计入,适合横向比较热度,不能直接等同于使用人数。

@atlaskit/top-layer 还在维护吗?

最新版本 7.1.5 发布于 2026/11/06(1 天前);至今共发布 97 个版本,其中过去 12 个月发布了 97 个;首次发布于 2026/03/25。发布频率只是参考信号:功能稳定的工具可能很少需要更新,建议结合源码仓库的 issue 和提交记录判断。