AdminKitDocs
Components

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.

Pro

This component is only available in AdminKit PRO. Learn more.

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. 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:

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:

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

OptionTypeDefaultDescription
messageReactNodeThe content of .toast-body
type"default" | "primary" | "secondary" | "success" | "info" | "warning" | "danger""default"The background color (text-bg-*); "default" is primary
durationnumber5000Milliseconds before it hides; 0 keeps it until it is closed
dismissiblebooleanfalseAdds 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:

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

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:

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>
    </>
  );
}
PropTypeDefaultDescription
showbooleanWhether the toast is shown
onClose() => voidCalled when the autohide delay has passed; set show to false here
onExited() => voidCalled once the toast has faded out
autohidebooleantrueHides the toast after delay
delaynumber5000Milliseconds from the end of the fade-in to onClose
classNamestringExtra 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 className="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".

Last updated on

On this page