# Overlays manager (/docs/overlays)





The **overlays manager** spawns [`Modal`](/docs/modal)s and [`Drawer`](/docs/drawer)s 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.

<Preview align="center" code="null">
  <ManagerDemo />
</Preview>

## Setup [#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:

```tsx title="app/overlays.ts"
'use client';
import { createOverlays } from '@42-central/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:

```tsx title="app/layout.tsx"
import { OverlaysProvider } from './overlays';

export default function Layout({ children }) {
  return (
    <body>
      {children}
      <OverlaysProvider />
    </body>
  );
}
```

Then call `overlays` from anywhere:

```tsx
import { overlays } from '@/app/overlays';

<Button onClick={() => overlays.open({ title: 'Hi', children: <Text>Hello!</Text> })}>
  Open
</Button>
```

## Content overlays [#content-overlays]

`overlays.open` spawns an overlay with any content and returns its `id`.

<Preview align="center" code="&#x22;overlays.open({\n  title: 'Subscribe to 42 events',\n  children: <Text>Any content you like.</Text>,\n});&#x22;">
  <OpenContentModal />
</Preview>

## Confirm overlays [#confirm-overlays]

`overlays.openConfirm` adds confirm/cancel buttons (dogfooded `Button`s) and renders as an `alertdialog`. The confirm button awaits an async `onConfirm`, showing a spinner until it resolves.

<Preview align="center" code="&#x22;overlays.openConfirm({\n  title: 'Delete project',\n  centered: true,\n  confirmProps: { color: 'red' },\n  labels: { confirm: 'Delete', cancel: 'Keep it' },\n  children: <Text>This cannot be undone.</Text>,\n  onConfirm: async () => { await deleteProject(); },\n});&#x22;">
  <div className="flex flex-wrap items-center gap-2">
    <OpenConfirmModal />

    <OpenDestructiveConfirm />
  </div>
</Preview>

## Drawers, from the same manager [#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).

<Preview align="center" code="&#x22;overlays.openDrawer({\n  title: 'Filters',\n  placement: 'end',\n  children: <FilterForm />,\n});&#x22;">
  <OpenManagerDrawer />
</Preview>

## Context overlays (typed) [#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.

```tsx title="app/overlays.ts"
import { createOverlays, type ContextOverlayProps } from '@42-central/ui-react/overlays';

function GreetModal({ context, params }: ContextOverlayProps<{ name: string }>) {
  return (
    <Flex>
      <Text>Hello, {params.name}!</Text>
      <Button onClick={() => context.close()}>Close</Button>
    </Flex>
  );
}

export const { overlays, OverlaysProvider } = createOverlays({
  registry: { greet: GreetModal },
});

// elsewhere — params is checked against GreetModal's props:
overlays.openContext({ key: 'greet', params: { name: 'Ada' } });
```

<Preview align="center" code="null">
  <OpenContextModal />
</Preview>

## Stacking & dynamic updates [#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.

<Preview align="center" code="&#x22;const id = overlays.open({\n  title: 'Processing…',\n  withCloseButton: false,\n  closeOnEscape: false,\n});\nsetTimeout(() => overlays.update(id, { title: 'Done!', withCloseButton: true }), 1500);&#x22;">
  <div className="flex flex-wrap items-center gap-2">
    <OpenStackedModals />

    <OpenUpdatingModal />

    <CloseAllButton />
  </div>
</Preview>

## Reading live state [#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.

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

```tsx
for (const entry of entries) {
  if (entry.kind === 'context' && entry.key === 'greet') {
    entry.params.name; // string — checked against GreetModal's props
  }
}
```

<Preview align="center" code="null">
  <LiveOverlayCount />
</Preview>

## API [#api]

`createOverlays(config)` config:

<TypeTable
  type="{
  registry: {
    type: 'Record<string, ComponentType<ContextOverlayProps<any>>>',
    description: 'Registered context-overlay components, keyed by string.',
  },
  labels: {
    type: '{ confirm: ReactNode; cancel: ReactNode }',
    description: 'Default confirm/cancel button labels.',
  },
  modalProps: {
    type: 'Partial<ModalProps>',
    description: 'Surface options merged under every modal.',
  },
  drawerProps: {
    type: 'Partial<DrawerProps>',
    description: 'Surface options merged under every drawer.',
  },
  container: {
    type: 'HTMLElement | null',
    description: 'Portal container shared by all overlays.',
  },
}"
/>

The returned `overlays` object:

<TypeTable
  type="{
  open: {
    type: '(input: ContentOverlayInput) => string',
    description: 'Open an overlay with free-form children. Returns its id.',
  },
  openConfirm: {
    type: '(input: ConfirmOverlayInput) => string',
    description: 'Open a confirm/cancel overlay (alertdialog).',
  },
  openContext: {
    type: '(input: { key; params } & SurfaceInput) => string',
    description: 'Open a registered context overlay with typed params.',
  },
  openDrawer: {
    type: '(input) => string',
    description: &#x22;Sugar for open({ ...input, surface: 'drawer' }).&#x22;,
  },
  close: {
    type: '(id: string) => void',
    description: 'Close one overlay by id (animates out).',
  },
  closeAll: {
    type: '() => void',
    description: 'Close every open overlay.',
  },
  update: {
    type: '(id: string, patch: Partial<SurfaceOptions>) => void',
    description: &#x22;Patch an open overlay's options (title, size, …).&#x22;,
  },
  updateContext: {
    type: '(id: string, params) => void',
    description: &#x22;Patch a context overlay's params.&#x22;,
  },
}"
/>

Every `open*` method accepts the target surface's props (`size`, `centered`, `placement`, `closeOnEscape`, …) inline, plus an optional `surface: 'modal' | 'drawer'`.
