Overlays manager
Spawn modals and drawers imperatively from anywhere — content, confirm and typed context overlays — with one centralized, type-safe manager.
The overlays manager spawns Modals and Drawers on the fly — no useState, no JSX wiring at the call site. It's inspired by @mantine/modals, but adapted to Ark UI: a vanilla store + a thin renderer (the same shape as Ark's createToaster), so the imperative overlays object is callable from anywhere — event handlers, utilities, even outside React.
Setup
createOverlays(config) returns a bound overlays object, an <OverlaysProvider /> renderer, and a useOverlays() hook. Call it once in a "use client" module and re-export the pieces:
'use client';
import { createOverlays } from '@42/ui-react/overlays';
export const { overlays, OverlaysProvider } = createOverlays({
labels: { confirm: 'Confirm', cancel: 'Cancel' },
});Mount <OverlaysProvider /> once near the root. It renders nothing until an overlay opens and is SSR-inert, so it's safe at the top of the tree:
import { OverlaysProvider } from './overlays';
export default function Layout({ children }) {
return (
<body>
{children}
<OverlaysProvider />
</body>
);
}Then call overlays from anywhere:
import { overlays } from '@/app/overlays';
<Button onClick={() => overlays.open({ title: 'Hi', children: <Text>Hello!</Text> })}>
Open
</Button>Content overlays
overlays.open spawns an overlay with any content and returns its id.
Code
overlays.open({title: 'Subscribe to 42 events',children: <Text>Any content you like.</Text>,});Confirm overlays
overlays.openConfirm adds confirm/cancel buttons (dogfooded Buttons) and renders as an alertdialog. The confirm button awaits an async onConfirm, showing a spinner until it resolves.
Code
overlays.openConfirm({title: 'Delete project',centered: true,confirmProps: { color: 'red' },labels: { confirm: 'Delete', cancel: 'Keep it' },children: <Text>This cannot be undone.</Text>,onConfirm: async () => { await deleteProject(); },});Drawers, from the same manager
One stack, one descriptor with a surface field — so the manager spawns drawers too. Use overlays.openDrawer, or pass surface: 'drawer' to any method (e.g. a confirm in a drawer).
Code
overlays.openDrawer({title: 'Filters',placement: 'end',children: <FilterForm />,});Context overlays (typed)
Register reusable overlay bodies by key, then open them with overlays.openContext. The registry type flows through generics, so params is type-checked against the registered component — no declare module augmentation.
import { createOverlays, type ContextOverlayProps } from '@42/ui-react/overlays';
function GreetModal({ context, params }: ContextOverlayProps<{ name: string }>) {
return (
<Stack>
<Text>Hello, {params.name}!</Text>
<Button onClick={() => context.close()}>Close</Button>
</Stack>
);
}
export const { overlays, OverlaysProvider } = createOverlays({
registry: { greet: GreetModal },
});
// elsewhere — params is checked against GreetModal's props:
overlays.openContext({ key: 'greet', params: { name: 'Ada' } });Stacking & dynamic updates
Overlays stack; overlays.closeAll() dismisses them all (each animates out independently). overlays.update(id, patch) patches an open overlay's options after it's shown.
Code
const id = overlays.open({title: 'Processing…',withCloseButton: false,closeOnEscape: false,});setTimeout(() => overlays.update(id, { title: 'Done!', withCloseButton: true }), 1500);Reading live state
createOverlays also returns a useOverlays() hook. Unlike the bound overlays
object — which is write-only handlers, callable from anywhere — useOverlays()
subscribes a component to the store and returns a [entries, overlays] tuple (à
la useState): the live overlay stack plus the same handlers.
const [entries, overlays] = useOverlays();
// reactive: re-renders as overlays open and close
const openCount = entries.filter((e) => e.status === 'open').length;entries is typed against your registry, so context entries discriminate on
key and expose typed params — no casting at the call site:
for (const entry of entries) {
if (entry.kind === 'context' && entry.key === 'greet') {
entry.params.name; // string — checked against GreetModal's props
}
}Open overlays: 0
[]API
createOverlays(config) config:
Prop
Type
The returned overlays object:
Prop
Type
Every open* method accepts the target surface's props (size, centered, placement, closeOnEscape, …) inline, plus an optional surface: 'modal' | 'drawer'.