# Next.js Toast Notifications

Next.js toast notifications for the App Router: a ToastProvider in the root layout and a useToast() hook for client components, built on Bootstrap's toast.

> This component is only available in **AdminKit PRO**. [Learn more](/pricing/#nextjs).

Toasts tell the user that something worked, or did not, without getting in their way. The message appears in a corner of the window and hides itself after a few seconds.

There are two ways to show one. The `useToast()` hook shows a toast with one function call. The `Toast` component is a single toast you place yourself. Both render Bootstrap's toast markup from React state, without Bootstrap's JavaScript. Toasts are part of the PRO version of [AdminKit for Next.js](/nextjs/). Their code is in `src/components/bootstrap/toast.tsx`.

The setup is already in place. `ToastProvider` wraps `{children}` in the root layout, `src/app/layout.tsx`, so it covers every route. The layout stays a server component: it renders on the server and has no state. `ToastProvider` is a client component, a file that starts with `"use client"`, because the toasts need state.

The toasts only exist in the browser. `ToastProvider` renders them in a portal on `document.body` after hydration, once the browser has taken over the HTML that Next.js built. So the server HTML has no toast container, and the server and the browser never disagree about the markup (no hydration mismatch).

## Example

`useToast()` reads a React context, which a server component cannot do. Call it in a client component and render that from your page:

```html
<button type="button" class="btn btn-primary" id="toastPublishButton">Publish</button>

<script>
document.getElementById("toastPublishButton").addEventListener("click", function () {
  showToast({
    message: "The post is live.",
    type: "success",
    dismissible: true
  });
});
</script>
```
```tsx code title="src/components/publish-button.tsx"
"use client";

import { useToast } from "@/components/bootstrap/toast";

export function PublishButton() {
  const showToast = useToast();

  return (
    <button
      type="button"
      className="btn btn-primary"
      onClick={() =>
        showToast({
          message: "The post is live.",
          type: "success",
          dismissible: true,
        })
      }
    >
      Publish
    </button>
  );
}
```

The page imports it and stays a server component:

```tsx title="src/app/(dashboard)/posts/page.tsx"
import { PublishButton } from "@/components/publish-button";
import { pageMetadata } from "@/lib/site";

export const metadata = pageMetadata("/posts");

export default function PostsPage() {
  return (
    <>
      <h1 className="h3 mb-3">Posts</h1>
      <PublishButton />
    </>
  );
}
```

Each toast fades in, hides itself after 5 seconds and is removed. `useToast()` throws an error outside `ToastProvider`. If you write your own root layout, keep `<ToastProvider>{children}</ToastProvider>` in it.

## Options

| Option | Type | Default | Description |
| --- | --- | --- | --- |
| `message` | `ReactNode` | | The content of `.toast-body` |
| `type` | `"default" \| "primary" \| "secondary" \| "success" \| "info" \| "warning" \| "danger"` | `"default"` | The background color (`text-bg-*`); `"default"` is primary |
| `duration` | `number` | `5000` | Milliseconds before it hides; `0` keeps it until it is closed |
| `dismissible` | `boolean` | `false` | Adds a close button |
| `position` | `{ x: "left" \| "center" \| "right"; y: "top" \| "bottom" }` | `{ x: "right", y: "top" }` | Where the toast appears in the window |

Toasts at the same position share one fixed `.toast-container`, newest last. The close button is white on every color except `warning` and `info`, which have dark text.

## Colors and positions

The color tells the user at a glance how things went. The position keeps the toast clear of what they are working on:

```html
<button type="button" class="btn btn-secondary me-2" id="toastSync">Bottom left</button>
<button type="button" class="btn btn-warning me-2" id="toastQuota">Bottom center</button>
<button type="button" class="btn btn-danger" id="toastOffline">Top right</button>

<script>
document.getElementById("toastSync").addEventListener("click", function () {
  showToast({ message: "Sync finished.", type: "secondary", position: { x: "left", y: "bottom" } });
});
document.getElementById("toastQuota").addEventListener("click", function () {
  showToast({ message: "You have used 90% of your storage.", type: "warning", position: { x: "center", y: "bottom" } });
});
document.getElementById("toastOffline").addEventListener("click", function () {
  showToast({ message: "You are offline.", type: "danger", position: { x: "right", y: "top" } });
});
</script>
```
```tsx code
<button
  type="button"
  className="btn btn-secondary me-2"
  onClick={() =>
    showToast({ message: "Sync finished.", type: "secondary", position: { x: "left", y: "bottom" } })
  }
>
  Bottom left
</button>
<button
  type="button"
  className="btn btn-warning me-2"
  onClick={() =>
    showToast({ message: "You have used 90% of your storage.", type: "warning", position: { x: "center", y: "bottom" } })
  }
>
  Bottom center
</button>
<button
  type="button"
  className="btn btn-danger"
  onClick={() =>
    showToast({ message: "You are offline.", type: "danger", position: { x: "right", y: "top" } })
  }
>
  Top right
</button>
```

## After a request

A toast is a good way to report how a form submission went. AdminKit for Next.js is a static export (`output: "export"`): the build turns every page into HTML files, with no Next.js server behind them. That means there are no Server Actions. A form sends its data with `fetch` from a client component instead, and shows the result in a toast. `showToast` is the same function on every render, so it is safe in callbacks and effects:

```tsx title="src/components/invite-form.tsx"
"use client";

import { useToast } from "@/components/bootstrap/toast";

export function InviteForm() {
  const showToast = useToast();

  async function onSubmit(event: React.FormEvent<HTMLFormElement>) {
    event.preventDefault();
    const response = await fetch("/api/invites", {
      method: "POST",
      body: new FormData(event.currentTarget),
    });
    if (response.ok) {
      showToast({ message: "Invite sent.", type: "success" });
    } else {
      showToast({ message: "The invite could not be sent.", type: "danger", duration: 0, dismissible: true });
    }
  }

  return (
    <form onSubmit={onSubmit} className="d-flex gap-2">
      <input name="email" type="email" className="form-control" aria-label="Email" required />
      <button type="submit" className="btn btn-primary">Invite</button>
    </form>
  );
}
```

## A single toast

For a toast with a header, or one you position yourself, use `Toast`: one toast with Bootstrap's fade. It needs state to know whether it is shown, so it goes in a client component too:

```html
<button type="button" class="btn btn-primary" id="toastBackupButton">Start backup</button>

<div class="toast mt-3" id="toastBackup" role="alert" aria-live="assertive" aria-atomic="true">
  <div class="toast-header">
    <strong class="me-auto">Backups</strong>
    <button type="button" class="btn-close" data-bs-dismiss="toast" aria-label="Close"></button>
  </div>
  <div class="toast-body">The backup is running. It takes a few minutes.</div>
</div>

<script>
document.getElementById("toastBackupButton").addEventListener("click", function () {
  bootstrap.Toast.getOrCreateInstance(document.getElementById("toastBackup")).show();
});
</script>
```
```tsx code title="src/components/start-backup.tsx"
"use client";

import { useCallback, useState } from "react";
import { CloseButton } from "@/components/bootstrap/close-button";
import { Toast } from "@/components/bootstrap/toast";

export function StartBackup() {
  const [show, setShow] = useState(false);
  const close = useCallback(() => setShow(false), []);

  return (
    <>
      <button type="button" className="btn btn-primary" onClick={() => setShow(true)}>
        Start backup
      </button>
      <Toast show={show} onClose={close} className="mt-3">
        <div className="toast-header">
          <strong className="me-auto">Backups</strong>
          <CloseButton onClick={close} />
        </div>
        <div className="toast-body">The backup is running. It takes a few minutes.</div>
      </Toast>
    </>
  );
}
```

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `show` | `boolean` | | Whether the toast is shown |
| `onClose` | `() => void` | | Called when the autohide delay has passed; set `show` to false here |
| `onExited` | `() => void` | | Called once the toast has faded out |
| `autohide` | `boolean` | `true` | Hides the toast after `delay` |
| `delay` | `number` | `5000` | Milliseconds from the end of the fade-in to `onClose` |
| `className` | `string` | | Extra classes for `.toast` |

Every change of `onClose` restarts the delay, so wrap the function in `useCallback`. For a toast in a corner, put it in `<div class="toast-container position-fixed top-0 end-0 p-3">`.

## Accessibility

- Toasts have `role="alert"`, `aria-live="assertive"` and `aria-atomic="true"`: screen readers announce them when they appear. Keep the message short.
- The focus stays where it is. Errors and messages with an action should not hide on their own: use `duration: 0` and `dismissible: true`, or a modal.
- The close button has `aria-label="Close"`.

## Related

- [React Bootstrap toasts](/react/docs/components/toasts/): the same component in AdminKit for React (Vite)
- [Bootstrap 5 toasts](/bootstrap/docs/components/toasts/): the toasts and `showToast()` of the HTML version
- [Alerts](/nextjs/docs/components/alerts/): messages that stay in the page
- [Notifications demo](https://nextjs-demo.adminkit.io/notifications)

---

Canonical HTML page: https://adminkit.io/nextjs/docs/components/toasts/

Site overview for agents: https://adminkit.io/llms.txt (expanded: https://adminkit.io/llms-full.txt) · Documentation: https://adminkit.io/bootstrap/docs/ · Questions: support@adminkit.io
