|Kit

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:

app/overlays.ts
'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:

app/layout.tsx
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.

app/overlays.ts
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'.

On this page