Popout
An anchored disclosure panel that ships zero JavaScript. The trigger is a
real <button popovertarget>; the panel is a native [popover] positioned by
CSS anchor positioning against the trigger's implicit anchor. The platform
supplies everything a JS popover engine used to: the top layer, light
dismiss, Esc, focus return, and collision flipping. It
renders as a Server Component and works with JavaScript disabled.
A zero-JS disclosure: top layer, Esc, light dismiss, and collision flipping all come from the platform.
import { Popout, Text } from "astralis-ui";
// No "use client". A fully interactive anchored panel with no JavaScript:
// a native [popover] invoked by popovertarget, positioned by CSS anchor
// positioning against the trigger's implicit anchor.
export function PopoutDemo() {
return (
<Popout>
<Popout.Trigger>What is this?</Popout.Trigger>
<Popout.Content side="bottom" align="start">
<Text size="sm">
A zero-JS disclosure: top layer, Esc, light dismiss, and collision
flipping all come from the platform.
</Text>
</Popout.Content>
</Popout>
);
}Import#
import { Popout } from "astralis-ui";Placement#
side and align compile to position-area; when there is no room, the
panel flips via position-try-fallbacks, the browser's collision handling,
not a resize listener:
import { Popout, HStack } from "astralis-ui";
export function PopoutPlacement() {
return (
<HStack gap="3" wrap="wrap" justifyContent="center">
{(["top", "bottom", "left", "right"] as const).map((side) => (
<Popout key={side}>
<Popout.Trigger>{side}</Popout.Trigger>
<Popout.Content side={side} align="center">
Anchored {side}; flips when there is no room.
</Popout.Content>
</Popout>
))}
</HStack>
);
}Popout is a disclosure, not a menu#
Deliberately: there is no roving focus and no menu ARIA, because those require state CSS cannot own. For an actions menu with arrow-key navigation, reach for Menu. That's what it's for. Popout is the right shape for nav dropdowns, info panels, and filters.
Browser support#
Popover is Baseline 2025; anchor positioning is cross-engine since January 2026 (Chrome 125+, Safari 26+, Firefox 147+). In browsers without anchor positioning the panel still opens, closes and light-dismisses correctly, and falls back to the browser's centered top-layer placement.
Props#
The root takes only its two parts; the panel is configured on Popout.Content:
| Prop | Type | Default | Description |
|---|---|---|---|
side | "top" | "bottom" | "left" | "right" | "bottom" | Which side of the trigger the panel opens on. |
align | "start" | "center" | "end" | "start" | Alignment along that side. |
sideOffset | number | 8 | Gap between the trigger and the panel, in pixels. |
Parts#
| Prop | Type | Default | Description |
|---|---|---|---|
Popout.Trigger | button props | None | Renders a button wired to the panel with popovertarget. |
Popout.Content | div props, plus the props above | None | The native popover panel. |
Keyboard#
All of it is the browser's popover behavior:
| Key | Action |
|---|---|
Enter / Space on the trigger | Open or close the panel |
Esc | Close the panel and return focus to the trigger |
Tab | Move through the panel's content, then on through the page |