@atlaskit/top-layer
v1.0.1
Published
Low-level layering primitives based on popover and dialog
Readme
@atlaskit/top-layer
Low-level top-layer primitives using the native
Popover API (popover="auto") and
<dialog> element.
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 — compose it with the useAnchorPosition hook when you need anchor-positioned content.
import { useRef } from 'react';
import { slideAndFade } from '@atlaskit/top-layer/animations';
import { getAriaForTrigger } from '@atlaskit/top-layer/get-aria-for-trigger';
import { Popover } from '@atlaskit/top-layer/popover';
import { PopoverSurface } from '@atlaskit/top-layer/popover-surface';
import { usePopoverId } from '@atlaskit/top-layer/use-popover-id';
import { useAnchorPosition } from '@atlaskit/top-layer/use-anchor-position';
function MyPopup({ isOpen, onClose }: { isOpen: boolean; onClose: () => void }) {
const triggerRef = useRef<HTMLButtonElement>(null);
const popoverRef = useRef<HTMLDivElement>(null);
const popoverId = usePopoverId();
useAnchorPosition({
anchorRef: triggerRef,
popoverRef,
placement: { axis: 'block', edge: 'end', align: 'start' },
});
return (
<>
<button
ref={triggerRef}
onClick={() => popoverRef.current?.togglePopover()}
{...getAriaForTrigger({ role: 'dialog', isOpen, popoverId })}
>
Open
</button>
<Popover
ref={popoverRef}
id={popoverId}
role="dialog"
label="Settings"
isOpen={isOpen}
animate={slideAndFade()}
onClose={onClose}
>
<PopoverSurface>Content here</PopoverSurface>
</Popover>
</>
);
}- Focus restoration is automatic. The browser handles it for outermost popovers;
Popoversnapshotsdocument.activeElementon open and restores it on close for nested cases. - For default overlay styling (background, shadow, border-radius), wrap content in
PopoverSurfacefrom@atlaskit/top-layer/popover-surface. - For trigger-less or custom-positioned UI (flag, tooltip, react-select menu portal), use
Popoveron its own withoutuseAnchorPosition.
Dialog
Compound component for modal dialogs using the native <dialog> element with .showModal().
{
isOpen && (
<Dialog onClose={handleClose} width="medium">
<Dialog.Content>
<Dialog.Surface>
<Dialog.Header>
<Dialog.Title>Heading</Dialog.Title>
<Dialog.CloseButton />
</Dialog.Header>
<Dialog.Body>Content</Dialog.Body>
<Dialog.Footer>Actions</Dialog.Footer>
</Dialog.Surface>
</Dialog.Content>
</Dialog>
);
}Animations
Both Popover and Dialog support CSS-based entry/exit animations via the animate prop.
Animations use @starting-style
for entry and
allow-discrete on
display/overlay for exit — no JavaScript animation coordination required.
Presets are available from @atlaskit/top-layer/animations:
import {
// Popover presets
slideAndFade,
fade,
scaleAndFade,
// Dialog presets
dialogSlideUpAndFade,
dialogFade,
} from '@atlaskit/top-layer/animations';Popover animation
Pass a preset to Popover:
import { slideAndFade } from '@atlaskit/top-layer/animations';
import { Popover } from '@atlaskit/top-layer/popover';
const animation = slideAndFade();
<Popover animate={animation} role="dialog" label="My popover">
Content
</Popover>;For overlay styling (background, shadow, border-radius), wrap children in PopoverSurface from
@atlaskit/top-layer/popover-surface.
Available popover presets:
slideAndFade({ distance? })— directional slide + opacity (default for tooltip)fade— simple opacity transitionscaleAndFade— scale from 0.95 + opacity (suitable for menus/dropdowns)
Dialog animation
Pass a preset to Dialog.Content:
import { dialogSlideUpAndFade } from '@atlaskit/top-layer/animations';
const animation = dialogSlideUpAndFade();
<Dialog.Content animate={animation}>
<Dialog.Surface>...</Dialog.Surface>
</Dialog.Content>;Available dialog presets:
dialogSlideUpAndFade({ distance? })— slide up + opacity with backdrop fade (matches legacy modal entrance)dialogFade— simple opacity + backdrop fade
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 criterion | Browser 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) | ✓ |
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.
