Docs
Overlays
0.2.0 adds a full overlay tier — dialogs, drawers, menus, popovers, context
menus, and tooltips. They all live behind @yoltra/ds/client (they need browser
APIs and portal to document.body), and each has its own stylesheet.
#Why they portal
Dialog, Drawer, and the anchored surfaces render through Portal into a node
under document.body, not where they're written. Rendering in place loses to CSS
three ways, and no z-index fixes any of them: an ancestor with overflow: hidden clips the panel; an ancestor with transform/filter becomes the
containing block for position: fixed; and an ancestor that established a
stacking context traps the overlay beneath whatever sits above that ancestor.
Portalling to the body leaves the overlay competing only with the document's own
stacking order — the --yl-z-* tokens. Their order encodes containment: a
popover opened inside a dialog sits above it, and a tooltip above them both.
#The modal tier — Dialog, Drawer
Both are controlled: open and onClose are the whole state contract, so the
surface never disagrees with your app about whether it's showing. They come with
the behaviour that makes an overlay usable rather than merely visible — focus
trapped inside and restored on close, page scroll locked (reference-counted), and
Escape / outside-press dismissal that stacks (one Escape closes the menu, not the
dialog behind it).
title is required — a modal with no accessible name is announced as "dialog"
and nothing else. Wrap it in VisuallyHidden if the design has no visible heading.
import { Dialog } from "@yoltra/ds/client";
import "@yoltra/ds/styles/modal.css";
<Dialog open={open} onClose={close} title="Search" size="lg">
<input type="search" placeholder="Search the docs…" />
</Dialog>;Drawer is the same machinery pinned to an edge — side="left" | "right" | "top" | "bottom" and a size (any CSS length). Reach for it when the content is a list
or a long form that a centred box would need its own scrollbar for anyway.
#The anchored tier — Menu, Popover, ContextMenu, Tooltip
These position against a trigger (or, for ContextMenu, a point). They're
non-modal: no focus trap, no scroll lock; they close on Escape, on an outside
press, and when focus leaves. The trigger render prop hands you the ARIA wiring
(aria-expanded, aria-haspopup, aria-controls) and the ref to anchor against
— spread it; the open state stays yours.
import { Menu, MenuItem } from "@yoltra/ds/client";
import "@yoltra/ds/styles/popover.css";
<Menu
open={open}
onClose={close}
label="Language"
trigger={(props) => (
<Button {...props} onClick={() => setOpen((v) => !v)}>EN</Button>
)}
>
<MenuItem onSelect={() => setLang("en")}>English</MenuItem>
<MenuItem onSelect={() => setLang("es")}>Español</MenuItem>
</Menu>;Menu implements the full keyboard pattern (arrows with wrap, Home/End, Enter/
Space, Tab to close). Popover is the same shell for arbitrary controls rather
than a command list. Tooltip is the exception — uncontrolled, since its
visibility belongs to the pointer and the focus ring, and wired with
aria-describedby (it supplements a control's name, it doesn't replace it):
import { Tooltip } from "@yoltra/ds/client";
import "@yoltra/ds/styles/tooltip.css";
<Tooltip content="Toggle theme">
{(props) => <IconButton {...props} label="Toggle theme">🌙</IconButton>}
</Tooltip>;Every overlay needs its stylesheet — modal.css for Dialog/Drawer,
popover.css for Menu/Popover/ContextMenu, tooltip.css for Tooltip —
plus the always-required tokens.css and base.css. See the
API reference for every prop.