# Overlays Three app-level singletons that float above the stage: a **tooltip**, a **modal**, and a **toast** stack. You call them imperatively from anywhere (`stageModal.open(...)`, `stageToast.push(...)`) — no ref threading, no context. The modal and toast each need a **one-time host component** mounted inside your `.stage-root`. The tooltip mounts itself. ```tsx function App() { return (
{/* once */} {/* once */}
); } ``` --- ## stageTooltip A single floating speech-bubble, positioned with `@floating-ui/dom`. It **mounts itself** — no host component. Mostly you don't call it directly: a field's `info` prop and its validation both drive it for you. Call it manually for custom anchored hints. ```ts stageTooltip.show({ anchor: myEl, // the element to point at level: 'info', // 'info' | 'success' | 'warn' | 'error' content: 'Press ⌘S to save', // string or JSX owner: myEl, // optional identity for owner-gated hide }); stageTooltip.hide(myEl); // gated: only hides if this owner holds it ``` | Method | Signature | Notes | |---|---|---| | `show` | `(opts: TooltipShowOptions) => void` | `{ anchor, level, content, owner?, keepOpen?, placement?, interactive?, onHide? }` | | `hide` | `(owner?) => void` | No-op unless that owner holds the bubble. | | `requestHide` | `(owner?) => void` | Immediate, owner-gated. | | `owns` | `(owner) => boolean` | | Auto-hide is automatic: the bubble closes the moment the pointer leaves the combined anchor+bubble box. `keepOpen?: () => boolean` suppresses that (a focused field passes `() => focused`). Four level skins tint the bubble. The bubble re-parents into the anchor's nearest `.stage-root` so tokens cascade. It hides by itself if its anchor is removed from the document or becomes inert, such as when the panel or section holding it collapses. `keepOpen` doesn't prevent either. Three more options suit tooltips on tightly packed targets, such as a grid of tiles: | Option | Default | Notes | |---|---|---| | `placement` | `'top'` | `'top' \| 'bottom' \| 'left' \| 'right'`. The preferred side; it still flips and shifts to stay on screen. | | `interactive` | `true` | `false` lets the pointer pass through the bubble to what's beneath, and the hover area becomes the anchor alone. Links in the content stop working. | | `onHide` | none | Called once per show session, as hiding starts rather than after the fade. Any hide ends a session, and so does another `show()`, even for the same owner. The old callback runs before the new session is set up, so you can clear one target's state and then set another's. | `owns()` stays true during the fade, so use `onHide` to keep state in step with the bubble. **Moving the bubble between anchors.** An outside `pointerdown` starts the fade, so a tap on another anchor that shows from `click` dips visibly for as long as the press lasts. To move the bubble without a dip, call `show()` from that anchor's `pointerdown`, which runs before anything paints. If a second tap on the same anchor means something, as in tap-to-activate, read "was it already active" at `pointerdown` too, or that tap's own `click` will see it as active. Moving between anchors by hover needs nothing extra, as long as the gap between them is no wider than 6px, the hover area's padding. --- ## stageModal One app-level modal for settings-style flows. Needs `` mounted once in your `.stage-root`. ```ts stageModal.open(() => stageModal.close()} />, { label: 'Settings' }); ``` > **`open` takes a *thunk* — `() => JSX.Element`, not pre-built JSX.** It's called > from event handlers (outside a Solid owner), so eager JSX would orphan its > effects. The host instantiates the thunk under its own owner. | Method | Signature | |---|---| | `open` | `(content: () => JSX.Element, options?: { label?: string; class?: string; closeOnInteractOutside?: boolean }) => void` | | `close` | `() => void` | | `isOpen` | `() => boolean` | `open` options: | Field | Type | Notes | |---|---|---| | `label?` | `string` | Accessible name for the dialog (typically the hosted section's title). | | `class?` | `string` | Class on the dialog container, for example to widen a content-heavy modal. | | `closeOnInteractOutside?` | `boolean` | Default `true` — a click on the backdrop closes the modal. Pass `false` for a **persistent** modal that only closes via the ×, Escape, or `stageModal.close()` (e.g. a flow with unsaved changes). | The host renders a dimming backdrop (clicks never pass through to content beneath) around a centred container. By default a click on the backdrop closes the modal; `closeOnInteractOutside: false` makes it persistent. The container hosts a single dismissable `Section` (its × calls close). It owns a11y: focus trap (Tab wraps within the modal), Escape closes, focus restored to the opener on close, `role=dialog` + `aria-modal`, and `aria-label` from `options.label`. Fields opened *inside* the modal portal into the modal container (so their select popovers/tooltips stack above the dim correctly) — this works automatically. ```tsx function SettingsDialog(props: { onDone: () => void }) { return (
); } ``` --- ## stageToast A corner stack of transient notifications. Needs `` mounted once in your `.stage-root`. ```ts const id = stageToast.push({ message: 'Saved', level: 'success' }); stageToast.push({ message: 'Could not connect', level: 'error', autoDismissMs: null }); // pinned stageToast.dismiss(id); stageToast.clear(); ``` | Method | Signature | Notes | |---|---|---| | `push` | `(opts: ToastOptions) => string` | Returns the toast id. | | `dismiss` | `(id: string) => void` | Animated fade-out. | | `clear` | `() => void` | Remove all. | `ToastOptions`: | Field | Type | Notes | |---|---|---| | `message` | `string \| (() => JSX.Element)` | A thunk renders rich content (a link/button). | | `level?` | `'info' \| 'success' \| 'warn' \| 'error'` | Default `'info'`. Tints the left border. | | `autoDismissMs?` | `number \| null` | Default `5000`; `null` pins it (no auto-dismiss). | A **thunk message** wires the toast into your app — its `onClick` can deep-link: ```ts stageToast.push({ level: 'warn', message: () => ( Unsaved changes. stageModal.open(SaveDialog)}>Review ), autoDismissMs: null, }); ``` Hovering any toast pauses the whole stack's auto-dismiss (so reading one never drops its neighbours). The × removes immediately; auto-dismiss and the API fade out. ### Host props ```tsx ``` | Prop | Type | Notes | |---|---|---| | `placement?` | `'top-left' \| 'top-right' \| 'bottom-left' \| 'bottom-right'` | Default `bottom-right`. | | `limit?` | `number` | Max visible; the excess collapses into a "+N more…" indicator. Default `5`. | | `hide?` | `'oldest' \| 'newest'` | Which end collapses on overflow. Default `oldest`. | | `contained?` | `boolean` | Use `position: absolute` instead of `fixed` — scope + clip toasts to a positioned region (e.g. a `CanvasStage` viewport) instead of the screen corner. | --- ## Z-order `--stage-z-popover` (1000, select/search dropdowns) < `--stage-z-modal` (1050) < `--stage-z-toast` (1060, so a toast fired from a modal shows over the dim) < `--stage-z-tooltip` (1100, always on top). A modal field's own tooltip still wins.