Portals and Layers
Use portals and layers with the current published Askr packages.
@askrjs/ui0.0.13@askrjs/themes0.0.13
How to use portals and layers
Portals and Layers should use the standard themed surface unless the application is deliberately implementing its own design system on top of @askrjs/ui.
- Import the themed component from @askrjs/themes/components; use @askrjs/ui directly only when building a custom visual system.
- Keep portals and layers labels, state, and application actions visible at the call site instead of styling internal DOM nodes.
- Verify keyboard behavior, focus movement or return, disabled state, and both standard themes.
import { Button, Dialog, DialogContent, DialogTitle, DialogTrigger } from '@askrjs/themes/components';
<Dialog>
<DialogTrigger asChild><Button>Open settings</Button></DialogTrigger>
<DialogContent>
<DialogTitle>Project settings</DialogTitle>
<ProjectSettings />
</DialogContent>
</Dialog>Purpose
Overlay content — dialogs, popovers, dropdowns, tooltips, menus — needs to render outside its trigger's normal position in the DOM to avoid being clipped by `overflow: hidden` ancestors or scoped `z-index` contexts. Every overlay family in @askrjs/ui ships a dedicated `Portal` component for exactly this: `DialogPortal`, `PopoverPortal`, `TooltipPortal`, `DropdownPortal`, `SelectPortal`, `MenubarPortal`, and `HoverCardPortal` all exist and are re-exported by the matching themed component.
Install and import
Portals ship alongside their parent component rather than as a separate import: `import { Dialog, DialogPortal, DialogContent } from '@askrjs/ui'` or the themed equivalent from `@askrjs/themes/components`. Themed aliases follow the same pattern — `DrawerPortal` and `SheetPortal`, for example, are literally the same `DialogPortal` re-exported under a different name.
Live examples
A typical Dialog composition wraps its overlay and content in `<DialogPortal><DialogOverlay /><DialogContent>...</DialogContent></DialogPortal>` — the portal moves that subtree to the end of the document body at render time, while everything else about the composition (FocusScope, DismissableLayer, the a11y contract) works exactly as it would inline.
Anatomy
`DialogPortalProps` is intentionally minimal — just `children` — because a portal's only job is relocating the DOM subtree it wraps, not adding behavior of its own. The behavior (open state, focus trapping, dismissal) stays owned by the surrounding Dialog, Popover, or Dropdown component; the portal is purely a rendering-location concern layered underneath it.
State model
Portals hold no state of their own — `DialogPortal` returns `JSX.Element | null` depending entirely on whether the parent's `open` state says there's anything to render. There's nothing to track independently; a portal simply reflects whatever its owning component's state already is.
Keyboard and accessibility
Moving markup to a different DOM location doesn't change its position in the accessibility tree the way it changes its visual position — portaled content still needs the same `aria-modal`, `aria-labelledby`, and focus-trap wiring the Dialog a11y contract specifies, and Askr's headless components handle that automatically regardless of where the portal renders to.
Styling and tokens
Stacking order across portaled layers is governed by a real, ordered `--ak-z-*` token ladder: `--ak-z-dropdown: 1000`, `--ak-z-sticky: 1100`, `--ak-z-fixed: 1200`, `--ak-z-modal-backdrop: 1300`, `--ak-z-modal: 1400`, `--ak-z-popover: 1500`, `--ak-z-toast: 1550`, and `--ak-z-tooltip: 1600`. Because every overlay family draws from this same ladder, a tooltip will always render above a popover, which will always render above a modal, without any manual z-index juggling in application code.
API
Every overlay family's Portal component takes the same shape: a `children`-only props type with no configuration surface, matching `DialogPortalProps`. The corresponding `BlockZIndex` type used elsewhere in the theme — `"header" | "sticky" | "fixed" | "dropdown" | "modal" | "popover" | "toast" | "tooltip"` — mirrors the same layer names as the `--ak-z-*` tokens, so Block-based layout and portal stacking stay in sync by construction.
Edge cases
Because portaled content renders outside its logical parent in the DOM, don't rely on CSS descendant selectors scoped to the trigger's container to style it — target the portal content's own `data-slot` hooks instead. Also be aware that nested portals (a dropdown opened from inside a dialog, for instance) stack according to the shared z-index ladder, not render order, so a component that opens later isn't guaranteed to render visually on top unless its layer token says it should.
Related pages
See Focus and Dismissal for how FocusScope and DismissableLayer coordinate with portaled content specifically, and Structures for how Block's own `zIndex` prop draws from the same token ladder used here.