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.

tsx
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.

tsx
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):

tsx
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.