# Toaster

> Transient notifications. A themed, RTL-aware Sonner Toaster plus the re-exported toast() function.

Source: https://docs.nasaqui.com/components/toast

## Install

```bash
npx shadcn@latest add https://docs.nasaqui.com/r/toast.json
```

Short-lived feedback after an action ("Project renamed", "Could not reach the server"). It wraps
[Sonner](https://sonner.emilkowal.ski): `Toaster` mounts the stack, themed from Nasaq tokens, and
`toast` is Sonner's function, re-exported. **You must mount `<Toaster />` yourself, once, near the app root**: without it `toast()` shows nothing.

## When to use

- Confirming a completed action, or reporting a background failure.
- A reversible action with an "Undo" button.

## When not to use

- Errors the user must fix in a form: show them on the field.
- Anything the user must decide on: use [`Dialog`](https://docs.nasaqui.com/components/dialog).
- Persistent notifications: use [`NotificationItem`](https://docs.nasaqui.com/components/notification-item) in a [`Sheet`](https://docs.nasaqui.com/components/sheet).

## Import

```tsx
import { Toaster, toast } from "@fadymondy/nasaq/web";
// inside this monorepo: "@nasaq/web"
```

## Quick start

```tsx
import { Button, Toaster, toast } from "@fadymondy/nasaq/web";

export function App() {
  return (
    <>
      <Button onClick={() => toast.success("Invoice sent")}>Send invoice</Button>
      <Toaster />
    </>
  );
}
```

`Toaster` is not mounted for you by `NasaqProvider` or `AppShell`; render it yourself, exactly once (two mounted Toasters show every toast twice).

## Apps that already use Sonner

Nasaq depends on `sonner` (exact version `2.0.8`); it is not a peer dependency, so that the toast works with no setup. A
`<Toaster>` only shows toasts from the same copy of Sonner, so an app that also imports `sonner` itself can end up with
two stores and need two Toasters. Pick one:

- **Use Nasaq's `toast` everywhere.** Import `toast` from `@fadymondy/nasaq/web` instead of `sonner`, and mount only Nasaq's
  `Toaster`. This always works.
- **Share one copy.** Pin your own `sonner` dependency to the same version (`2.0.8`) so the package manager installs one
  copy; then `toast` from either import reaches either `Toaster`. Check with `npm ls sonner` or `pnpm why sonner`.

## Anatomy

```
Toaster                     Sonner Toaster (fixed stack, bottom inline-end)
└─ toast                    Sonner toast, styled via toastOptions.classNames
   ├─ icon                  [data-icon], coloured for success / error / warning
   ├─ title
   ├─ description
   └─ action / cancel       buttons
```

There is no Nasaq `data-slot`; Sonner's own attributes (`data-sonner-toast`, `data-type`) apply.

## API

### `Toaster`

`ComponentProps<typeof Sonner>`: all Sonner `Toaster` props pass through (`duration`, `visibleToasts`,
`closeButton`, `expand`, `richColors`, `offset`, `toastOptions`, ...). Set from Nasaq context (when a
`NasaqProvider` is present):

| Prop | Value | Description |
| --- | --- | --- |
| `theme` | `resolvedTheme` or `"system"` | Follows the Nasaq light/dark theme. |
| `dir` | `"rtl"` or `"ltr"` | Follows `isRtl`. |
| `position` | `"bottom-left"` in RTL, else `"bottom-right"` | Inline-end corner. |
| `containerAriaLabel` | `"Notifications"` or `"الإشعارات"` | Accessible name of the toast region; Arabic when the Nasaq locale starts with `ar`. |
| `toastOptions` | Nasaq `classNames`, merged with yours | Themed surface, description, action, cancel and status icon colours. |

Props you pass are spread after these defaults and override them. `toastOptions` is the exception: your
other options (`duration`, `style`, ...) pass through, and each of your `toastOptions.classNames` entries is
merged (`cn`) after the Nasaq class for the same slot, so you extend the theme instead of wiping it.

### `toast`

Sonner's function, unchanged.

| Call | Use |
| --- | --- |
| `toast(message, options?)` | Neutral. |
| `toast.success(message)` | Success (icon uses `nq-success-text`). |
| `toast.error(message)` | Error (icon uses `nq-danger-text`). |
| `toast.warning(message)` | Warning (icon uses `nq-warning-text`). |
| `toast.info(message)` | Info. |
| `toast.loading(message)` | Pending state. |
| `toast.promise(promise, { loading, success, error })` | Follows a promise. |
| `toast.dismiss(id?)` | Dismisses one or all. |

Common `options`: `description`, `duration`, `id`, `action: { label, onClick }`, `cancel: { label, onClick }`.

## Examples

### Undo action

```tsx
import { Button, Toaster, toast } from "@fadymondy/nasaq/web";

export function DeleteWithUndo() {
  return (
    <>
      <Button
        onClick={() =>
          toast("Issue deleted", { action: { label: "Undo", onClick: () => toast("Restored") } })
        }
      >
        Delete issue
      </Button>
      <Toaster />
    </>
  );
}
```

### Promise, Arabic copy

```tsx
import { Button, Toaster, toast } from "@fadymondy/nasaq/web";

export function SendAr() {
  return (
    <>
      <Button
        variant="primary"
        onClick={() =>
          toast.promise(new Promise((r) => setTimeout(r, 1200)), {
            loading: "جارٍ الإرسال…",
            success: "تم إرسال الفاتورة",
            error: "تعذّر الاتصال بالخادم",
          })
        }
      >
        إرسال الفاتورة
      </Button>
      <Toaster />
    </>
  );
}
```

## Accessibility

Sonner renders a labelled `<section aria-live="polite">` region; each toast is announced by screen readers.

| Key | Action |
| --- | --- |
| `Alt+T` | Focuses the toast region (Sonner default hotkey). |
| `Tab` | Moves to the action / cancel / close buttons of a toast. |

- Toasts auto-dismiss; do not rely on them for anything the user must read. Persist important results elsewhere.
- Every status toast should carry text, not only an icon or colour.
- The region label is localised through `containerAriaLabel` ("Notifications" / "الإشعارات"); pass your own to override.
- Localise the message, action and cancel labels.

## RTL & i18n

- With `NasaqProvider`, the stack moves to the bottom-left in RTL and toast content flows right to left.
- Without a provider it defaults to LTR, bottom-right.
- The only built-in string is the region label (`containerAriaLabel`), English or Arabic by Nasaq locale.

## Styling & tokens

- Surface: `bg-popover`, `text-popover-foreground`, `border-border`, `rounded-floating`, `text-body-sm`. Shadow: `shadow-floating`, as other overlays.
- Action button: `bg-primary`; cancel: `bg-secondary`. Icon colours: `nq-success-text`, `nq-danger-text`, `nq-warning-text`.
- Sonner classes are overridden with `!` important modifiers. Extend through `toastOptions.classNames` (merged with ours) or `className`.

## Do / Don't

- **Do** keep messages short and specific ("Invoice sent").
- **Do** offer Undo instead of a confirm dialog for reversible actions.
- **Don't** use toasts for errors that need action; keep them inline.
- **Don't** fire several toasts for one action.
- **Don't** mount more than one `<Toaster />`.

## Related

- [Dialog](https://docs.nasaqui.com/components/dialog) · [NotificationItem](https://docs.nasaqui.com/components/notification-item) · [Status](https://docs.nasaqui.com/components/status)

## Lab

https://docs.nasaqui.com/?path=/docs/components-alerts-notifications-toast--docs

## Code

### React

```tsx
import { Button, Toaster, toast } from "@fadymondy/nasaq/web";

export function App() {
  return (
    <>
      <Button onClick={() => toast.success("Invoice sent")}>Send invoice</Button>
      <Toaster />
    </>
  );
}
```

### shadcn

```tsx
import { Button } from "@/components/ui/button";
import { Toaster, toast } from "@/components/ui/toast";

export function App() {
  return (
    <>
      <Button onClick={() => toast.success("Invoice sent")}>Send invoice</Button>
      <Toaster />
    </>
  );
}
```

### Vue

```vue
<script setup lang="ts">
import { NqButton, NqToaster, toast } from "@fadymondy/nasaq/vue";
</script>

<template>
  <NqButton @click="toast.success('Invoice sent')">Send invoice</NqButton>
  <NqToaster />
</template>
```

### Blade

```blade
<x-nq::button x-on:click="nqToast.success('Invoice sent')">Send invoice</x-nq::button>
<x-nq::toast />
```

### HTML + Alpine

```html
<button data-slot="button"
     type="button"                         x-on:click="nqToast.success(&#039;Invoice sent&#039;)" class="inline-flex shrink-0 select-none items-center justify-center gap-2 whitespace-nowrap rounded-control border font-sans text-label transition-colors duration-150 ease-nq min-h-[var(--nq-touch-min,0px)] outline-none focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-nq-focus disabled:pointer-events-none disabled:opacity-50 data-disabled:pointer-events-none data-disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0 border-border bg-card text-foreground hover:bg-nq-hover h-control px-[var(--nq-control-pad)]">
        Send invoice</button>
<section data-slot="toaster" x-data="nqToaster(4000)" x-bind="region" tabindex="-1"
    aria-label="Notifications" aria-live="polite"
    class="pointer-events-none fixed z-[100] w-[min(356px,calc(100vw-2rem))] bottom-4 end-4">
    <ol class="m-0 flex list-none flex-col gap-2 p-0">
        <template x-for="t in toasts" :key="t.id">
            <li data-slot="toast" role="status" :data-type="t.type"
                x-transition:enter="transition duration-200 ease-nq" x-transition:enter-start="opacity-0 translate-y-2" x-transition:enter-end="opacity-100 translate-y-0"
                x-transition:leave="transition duration-150 ease-nq" x-transition:leave-start="opacity-100" x-transition:leave-end="opacity-0"
                class="pointer-events-auto relative flex items-start rounded-floating border border-border bg-popover p-4 font-sans text-body-sm text-popover-foreground shadow-floating gap-2">
                <span data-icon aria-hidden="true" class="mt-0.5 inline-flex shrink-0 [&_svg]:size-4" x-show="t.type !== 'message'">
                    <svg x-show="t.type === 'success'" class="text-nq-success-text" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
  <circle cx="12" cy="12" r="10"/>
  <path d="m9 12 2 2 4-4"/>
</svg>                    <svg x-show="t.type === 'error'" class="text-nq-danger-text" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
  <circle cx="12" cy="12" r="10"/>
  <path d="m15 9-6 6"/>
  <path d="m9 9 6 6"/>
</svg>                    <svg x-show="t.type === 'warning'" class="text-nq-warning-text" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
  <circle cx="12" cy="12" r="10"/>
  <line x1="12" x2="12" y1="8" y2="12"/>
  <line x1="12" x2="12.01" y1="16" y2="16"/>
</svg>                    <svg x-show="t.type === 'info'" class="text-nq-info-text" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
  <circle cx="12" cy="12" r="10"/>
  <path d="M12 16v-4"/>
  <path d="M12 8h.01"/>
</svg>                </span>
                <div class="flex min-w-0 flex-1 flex-col gap-0.5">
                    <div data-title class="font-medium" x-text="t.title"></div>
                    <div data-description class="text-muted-foreground" x-show="t.description" x-text="t.description"></div>
                </div>
                <button type="button" data-close x-on:click="dismiss(t.id)" aria-label="Close"
                    class="-me-1 -mt-1 inline-flex size-6 shrink-0 items-center justify-center rounded-control text-muted-foreground transition-colors duration-150 hover:bg-nq-hover hover:text-foreground focus-visible:outline-2 focus-visible:outline-nq-focus [&_svg]:size-4"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
  <path d="M18 6 6 18"/>
  <path d="m6 6 12 12"/>
</svg></button>
            </li>
        </template>
    </ol>
</section>
```
