# Notifier (/docs/notifier)





The **Notifier** is the imperative toast manager. You **create one**, mount its region **once** near the app root, then call `notify.*` from anywhere — an event handler, a data mutation, a background task — with no `useState` or JSX at the call site. Each live toast is rendered as a [`Notification`](/docs/notification) card.

It's a custom toaster modelled on the kit's [`createOverlays`](/docs/overlays): a framework-agnostic store + a once-mounted renderer + a bound controller. [Ark UI's Presence](https://ark-ui.com/) drives each toast's enter/exit lifecycle; everything else — the queue, the timers, placement, the deck↔column stacking, hover-pause, swipe-to-dismiss — is the kit's own, with the display and animation pushed into CSS (so heights animate, and it all respects `prefers-reduced-motion`).

## Setup [#setup]

`createNotifier(options)` returns a bound `notify` object and a `<Notifications />` renderer. Call it once in a `"use client"` module and re-export the pieces:

```tsx title="app/notifier.ts"
'use client';
import { createNotifier } from '@42-central/ui-react/notification';

export const { notify, Notifications } = createNotifier({ placement: 'bottom-end' });
```

Mount `<Notifications />` once near the root. It renders nothing until a toast opens and is SSR-inert, so it's safe at the top of the tree:

```tsx title="app/layout.tsx"
import { Notifications } from './notifier';

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

Then call `notify` from anywhere:

```tsx
import { notify } from '@/app/notifier';

notify('Copied to clipboard');            // neutral toast
notify.success('Saved');
```

## Status presets [#status-presets]

Five presets — `success`, `error`, `warning`, `info`, `loading` — each maps to a hue (green / red / orange / neutral / neutral) and a default leading icon (`loading` shows a spinner and never auto-dismisses). Pass a bare string for the title, or an options object for more.

<Preview align="center" code="&#x22;notify.success('Changes saved');\nnotify.error('Something went wrong');\nnotify.warning('Double-check your input');\nnotify.info('A new version is available');\nnotify.loading('Syncing…');&#x22;">
  <StatusDemo />
</Preview>

## With a description [#with-a-description]

Pass an options object with `title` and `description` for a secondary line. It reveals with a [`Collapse`](/docs/collapse) height animation.

<Preview align="center" code="&#x22;notify.success({\n  title: 'Profile updated',\n  description: 'Your changes are live across all 42 campuses.',\n});&#x22;">
  <DescriptionDemo />
</Preview>

## Action [#action]

`action` is &#x2A;*any `ReactNode`** — its own section under the toast, revealed independently of the description. Compose it however you like; the kit ships `Notification.ActionTrigger` for the standard button styling. Keep the id `notify.*` returns to dismiss the toast from inside the handler — good for an **Undo**.

<Preview align="center" code="&#x22;const id = notify.info({\n  title: 'Message archived',\n  action: (\n    <Notification.ActionTrigger\n      onClick={() => {\n        notify.success('Message restored');\n        notify.dismiss(id);\n      }}\n    >\n      Undo\n    </Notification.ActionTrigger>\n  ),\n});&#x22;">
  <ActionDemo />
</Preview>

## Promise [#promise]

`notify.promise` drives a **single** toast through an async lifecycle: it starts as `loading`, then transitions to `success` or `error` when the promise settles. `success` / `error` may be functions of the resolved value / thrown error.

<Preview align="center" code="&#x22;notify.promise(upload(), {\n  loading: 'Uploading…',\n  success: (name) => ({ title: 'Upload complete', description: name }),\n  error: 'Upload failed — please try again',\n});&#x22;">
  <PromiseDemo />
</Preview>

## Updating a toast [#updating-a-toast]

Every spawn returns an **id**. Keep it and call `notify.update(id, …)` to patch a live toast — only the fields you pass change; anything you already set is kept, and the status type is preserved. To change the **status** too (say a manual `loading → success`), re-call the status helper with the same `id`. Because the description and action are independent `Collapse` sections, adding or removing either **animates its height** rather than jumping.

<Preview align="center" code="&#x22;const id = notify.loading('Uploading…');\n\n// patch in place — keeps everything else\nnotify.update(id, { description: 'Halfway there…' });\n\n// …or transition its status (resets to the success preset)\nnotify.success({ id, title: 'Uploaded' });&#x22;">
  <UpdateDemo />
</Preview>

## Custom color [#custom-color]

Every toast shares one outline surface; `color` is the only styling knob. A status helper picks the hue for you, so it doesn't take a `color` (passing one is a type error). Call `notify(...)` directly for a toast with no status semantics and give it any palette `color`; pass `icon: null` to drop the leading glyph.

<Preview align="center" code="&#x22;// fully custom — no status, no icon\nnotify({\n  title: 'Pro tip',\n  description: 'Press ⌘K to search anywhere.',\n  color: 'purple',\n  icon: null,\n});&#x22;">
  <CustomDemo />
</Preview>

## Placement & stacking [#placement--stacking]

`placement` (a `createNotifier` option) anchors the region to any edge or corner. With `expand` (default), toasts collapse into a tidy **deck** — older cards peek behind the newest, tops aligned — that **expands into a column on hover**; hovering also **pauses** the auto-dismiss timers. `max` caps how many render at once.

Because placement belongs to the notifier (not an individual toast), each button below is its own `createNotifier` instance.

<Preview align="center" code="`const { notify, Notifications } = createNotifier({ placement: 'top-end', expand: true, max: 5 });`">
  <PlacementDemo />
</Preview>

## Behaviors [#behaviors]

* **Auto-dismiss** after `duration` (default 5s); `loading` toasts never expire.
* **Pause** while the pointer is over the stack, and while the tab is hidden — so nothing expires unseen.
* **Swipe / drag** a toast aside to dismiss it, or **click** it. Presses that start on the close button or an action are left to that control.
* **Mobile** (below the `xs` breakpoint) the region pins to the bottom, full-width, above the safe area — regardless of `placement`.

## Playground [#playground]

The controls below build a single toast and spawn it on click. Notifier-level options (`placement` / `max` / `expand`) are set once at creation, so tune those in your `createNotifier` call.

<story.Playground />

## API [#api]

<Callout type="info" title="No testId on notify.* calls">
  Unlike the rest of the kit, `notify(...)` / `notify.success(...)` / etc. don't take a `testId` — there's no props object rendered as a component here to thread one through, just a plain input object consumed by an imperative call. Each call already returns an **id** (see `notify.update` / `notify.dismiss` above) for referencing a specific toast from code; to find one in the DOM, query by role/text per the [Testing](/docs/testing) guide, or reach for `Notification.Root`'s own `testId` when you compose a bespoke toast body by hand.
</Callout>

`createNotifier(options)` — configures the notifier and its region:

<TypeTable
  type="{
  placement: { type: '&#x22;top-start&#x22; | &#x22;top&#x22; | &#x22;top-end&#x22; | &#x22;bottom-start&#x22; | &#x22;bottom&#x22; | &#x22;bottom-end&#x22;', default: '&#x22;bottom-end&#x22;', description: 'Edge/corner the region anchors to.' },
  max: { type: 'number', default: '5', description: 'Max toasts on screen before the oldest is dropped.' },
  duration: { type: 'number', default: '5000', description: 'Default auto-dismiss duration (ms); loading never auto-dismisses.' },
  expand: { type: 'boolean', default: 'true', description: 'Collapse into a deck that expands into a column (and pauses timers) on hover.' },
  withinPortal: { type: 'boolean', default: 'true', description: 'Render the region in a Portal so it escapes clipping ancestors.' },
  container: { type: 'HTMLElement | null', description: 'Portal target. Defaults to document.body.' },
}"
/>

`<Notifications />` — the region renderer `createNotifier` returns:

<TypeTable
  type="{
  translations: {
    type: '{ regionLabel?: string; closeTriggerLabel?: string; collapseLabel?: string; expandLabel?: string }',
    description: &#x22;regionLabel names the toast region (default \&#x22;Notifications\&#x22;); the other keys are Notification's own, forwarded to every toast. A partial override keeps the other defaults.&#x22;,
  },
}"
/>

```tsx
<Notifications translations={{ regionLabel: 'Alertes', closeTriggerLabel: 'Fermer' }} />
```

`notify(input)` and `notify.success/error/warning/info/loading(input)` — where `input` is a title node or these options:

<TypeTable
  type="{
  title: { type: 'ReactNode', description: 'The toast heading. A bare string/element passed to notify.* is shorthand for { title }.' },
  description: { type: 'ReactNode', description: 'Secondary line under the title.' },
  action: { type: 'ReactNode', description: 'An action section under the toast — any node, revealed independently of the description.' },
  duration: { type: 'number', description: 'Visible duration in ms; omit for the notifier default (loading never auto-dismisses).' },
  withCloseButton: { type: 'boolean', default: 'true', description: 'Render the close (×) button.' },
  color: { type: 'Color', description: 'Accent palette. notify(...) only: the status helpers set the hue themselves and refuse a color, and so does notify.update (re-call notify({ id, color }) to recolor).' },
  icon: { type: 'ReactNode | null', description: 'Custom leading glyph; null hides the default status icon.' },
  id: { type: 'ToastId', description: 'Provide to update or dedupe an existing toast (a returned id, or your own cast: &#x22;upload&#x22; as ToastId).' },
}"
/>

The bound `notify` object also carries: `notify.promise(promise, { loading, success?, error? })`, `notify.update(id, input)`, `notify.dismiss(id?)` (one toast, or all if omitted), `notify.remove(id)` (drop immediately, no exit), `notify.has(id)`, and `notify.getToasts()`.
