Show notifications with actions, timed dismissal, and screen reader announcements.

A toast tells someone that something happened, like a saved change or a sent message, and goes away on its own. It can offer an action such as Undo, stacks with the toasts before it, and is announced to screen readers without taking focus.

Console
<script lang="ts">
  import { onDestroy } from "svelte"
  import {
    Toast,
    ToastAction,
    ToastDescription,
    ToastRegion,
    ToastTitle,
    createToaster,
  } from "@ui-sdk/svelte/toast"
  import { useLog } from "./log"

  const log = useLog()
  const toast = createToaster()
  onDestroy(() => {
    toast.dismiss()
  })
</script>

<button
  onclick={() =>
    toast("Event created", {
      description: "Friday, June 12 at 10:00 AM",
      action: { label: "Undo", onAction: () => log("Event removed") },
    })}
>
  Create Event
</button>
<ToastRegion toaster={toast}>
  <Toast>
    <div data-slot="stack">
      <ToastTitle />
      <ToastDescription />
    </div>
    <ToastAction data-variant="solid" />
  </Toast>
</ToastRegion>
Press Create Event. Choose Undo, swipe the toast away, or wait for it to close. Undo logs Event removed.

Installation

npm install @ui-sdk/svelte @ui-sdk/core

Composition

Use the following composition to build a ToastRegion:

ToastRegion
├── Toast
│   ├── ToastTitle
│   ├── ToastDescription
│   ├── ToastAction
│   └── ToastClose
└── Toast
    ├── ToastTitle
    └── ToastDescription

The region renders one Toast for each toast in its queue, newest first, through its children snippet. A toast is named by its title and described by its description. Empty title, description and action parts show the text from the toast's data, so a region's renderer is usually the same few parts.

Usage

Import the parts and put them together, then call toast() when an action finishes:

<script lang="ts">
  import {
    Toast,
    ToastDescription,
    ToastRegion,
    ToastTitle,
    toast,
  } from "@ui-sdk/svelte/toast"
</script>

<button
  type="button"
  onclick={() => toast("Event created", { description: "Friday at 10:00 AM" })}
>
  Create Event
</button>
<ToastRegion>
  <Toast>
    <ToastTitle />
    <ToastDescription />
  </Toast>
</ToastRegion>

toast() works from anywhere, not only from components, and returns the toast's id. Place the region once, near your app's root. Without content of its own, it renders each toast's title, description, action and cancel button with unstyled parts.

The examples below each call createToaster() for a queue of their own and pass it to their region as toaster, so their toasts don't mix.

Types

toast.success, .info, .warning, .error and .loading set the toast's type, which the toast exposes as data-type for an icon or a color. Errors are announced right away and other types wait for a pause; set priority to change that. A loading toast stays until it's updated or dismissed.

<script lang="ts">
  import { onDestroy } from "svelte"
  import {
    CircleCheckIcon,
    InfoIcon,
    LoaderCircleIcon,
    OctagonXIcon,
    TriangleAlertIcon,
  } from "@lucide/svelte"
  import {
    Toast,
    ToastRegion,
    ToastTitle,
    createToaster,
  } from "@ui-sdk/svelte/toast"

  function upload() {
    return new Promise((resolve) => setTimeout(resolve, 2000))
  }

  const toast = createToaster()
  onDestroy(() => {
    toast.dismiss()
  })
</script>

<div data-slot="row">
  <button onclick={() => toast("Event created")}>Default</button>
  <button onclick={() => toast.success("Changes saved")}>Success</button>
  <button onclick={() => toast.info("A new version is available")}>Info</button>
  <button onclick={() => toast.warning("Storage is almost full")}
    >Warning</button
  >
  <button onclick={() => toast.error("Couldn't save changes")}>Error</button>
  <button
    onclick={() =>
      toast.promise(upload, {
        loading: "Uploading photos…",
        success: "Photos uploaded",
        error: "Upload failed",
      })}
  >
    Loading
  </button>
</div>
<ToastRegion toaster={toast}>
  <Toast>
    <CircleCheckIcon data-show="success" aria-hidden="true" />
    <InfoIcon data-show="info" aria-hidden="true" />
    <TriangleAlertIcon data-show="warning" aria-hidden="true" />
    <OctagonXIcon data-show="error" aria-hidden="true" />
    <LoaderCircleIcon data-show="loading" aria-hidden="true" />
    <ToastTitle />
  </Toast>
</ToastRegion>
Choose a type. Each toast shows its type's icon, and Loading turns into Photos uploaded when it finishes.

Position

Set the region's side to top or bottom, and align to start, center or end. Start and end follow the writing direction. A toast can be swiped toward the side and, unless it's centered, toward the end it's aligned to.

<script lang="ts">
  import { onDestroy } from "svelte"
  import { CheckIcon, ChevronDownIcon } from "@lucide/svelte"
  import {
    Select,
    SelectContent,
    SelectItem,
    SelectItemIndicator,
    SelectLabel,
    SelectTrigger,
    SelectValue,
  } from "@ui-sdk/svelte/select"
  import {
    Toast,
    ToastDescription,
    ToastRegion,
    ToastTitle,
    createToaster,
  } from "@ui-sdk/svelte/toast"

  const sides = [
    { value: "top", label: "Top" },
    { value: "bottom", label: "Bottom" },
  ]
  const aligns = [
    { value: "start", label: "Start" },
    { value: "center", label: "Center" },
    { value: "end", label: "End" },
  ]
  type Side = "top" | "bottom"
  type Align = "start" | "center" | "end"

  const toast = createToaster()
  let side = $state<Side>("bottom")
  let align = $state<Align>("end")
  onDestroy(() => {
    toast.dismiss()
  })
</script>

<div data-slot="stack">
  <div data-slot="controls">
    <Select
      items={sides}
      value={side}
      onValueChange={(next) => next && (side = next as Side)}
    >
      <SelectLabel>Side</SelectLabel>
      <SelectTrigger>
        <SelectValue />
        <ChevronDownIcon aria-hidden="true" />
      </SelectTrigger>
      <SelectContent>
        {#each sides as option (option.value)}
          <SelectItem value={option.value}>
            {option.label}
            <SelectItemIndicator>
              <CheckIcon aria-hidden="true" />
            </SelectItemIndicator>
          </SelectItem>
        {/each}
      </SelectContent>
    </Select>
    <Select
      items={aligns}
      value={align}
      onValueChange={(next) => next && (align = next as Align)}
    >
      <SelectLabel>Align</SelectLabel>
      <SelectTrigger>
        <SelectValue />
        <ChevronDownIcon aria-hidden="true" />
      </SelectTrigger>
      <SelectContent>
        {#each aligns as option (option.value)}
          <SelectItem value={option.value}>
            {option.label}
            <SelectItemIndicator>
              <CheckIcon aria-hidden="true" />
            </SelectItemIndicator>
          </SelectItem>
        {/each}
      </SelectContent>
    </Select>
  </div>
  <button
    onclick={() =>
      toast("Event created", { description: "Friday, June 12 at 10:00 AM" })}
  >
    Create Event
  </button>
  <ToastRegion toaster={toast} {side} {align}>
    <Toast>
      <div data-slot="stack">
        <ToastTitle />
        <ToastDescription />
      </div>
    </Toast>
  </ToastRegion>
</div>
Choose a side and an alignment, then press Create Event and look for the toast at that edge of the page.

Action and Cancel

Add an action, like Undo, and a cancel button that dismisses the toast. Their callbacks can update your app. onDismiss tells you why the toast is closing, and details.cancel() keeps it open; a canceled timeout starts the countdown again.

Console
<script lang="ts">
  import { onDestroy } from "svelte"
  import {
    Toast,
    ToastAction,
    ToastClose,
    ToastRegion,
    ToastTitle,
    createToaster,
  } from "@ui-sdk/svelte/toast"
  import { useLog } from "./log"

  const log = useLog()
  const toast = createToaster()
  onDestroy(() => {
    toast.dismiss()
  })
</script>

<button
  onclick={() =>
    toast("Message archived", {
      action: { label: "Undo", onAction: () => log("Message restored") },
      cancel: { label: "Dismiss", onCancel: () => log("Kept archived") },
      onDismiss: ({ reason }) => log(`Closed (${reason})`),
    })}
>
  Archive Message
</button>
<ToastRegion toaster={toast}>
  {#snippet children(item)}
    <Toast>
      <ToastTitle />
      {#if item.cancel}
        <ToastClose action={item.cancel.onCancel}
          >{item.cancel.label}</ToastClose
        >
      {/if}
      <ToastAction data-variant="solid" />
    </Toast>
  {/snippet}
</ToastRegion>
Press Archive Message, then choose Undo or Dismiss, swipe the toast away, or let it time out. The console logs why it closed.

Give an action an href to render it as a link. It keeps normal link behavior, including opening in a new tab.

<script lang="ts">
  import { onDestroy } from "svelte"
  import {
    Toast,
    ToastAction,
    ToastDescription,
    ToastRegion,
    ToastTitle,
    createToaster,
  } from "@ui-sdk/svelte/toast"

  const toast = createToaster()
  onDestroy(() => {
    toast.dismiss()
  })
</script>

<button
  onclick={() =>
    toast("Invoice sent", {
      description: "Hollis Grant · $240.00",
      action: { label: "View", href: "#" },
    })}
>
  Send Invoice
</button>
<ToastRegion toaster={toast}>
  <Toast>
    <div data-slot="stack">
      <ToastTitle />
      <ToastDescription />
    </div>
    <ToastAction data-variant="solid" />
  </Toast>
</ToastRegion>
Press Send Invoice, then follow View in the toast.

Promise

toast.promise() shows a loading toast, then updates the same toast when the promise resolves or rejects. Each message can be a string, options with a description or an action, or a function of the result. A toast dismissed while loading stays closed.

<script lang="ts">
  import { onDestroy } from "svelte"
  import {
    Toast,
    ToastAction,
    ToastDescription,
    ToastRegion,
    ToastTitle,
    createToaster,
  } from "@ui-sdk/svelte/toast"

  const toast = createToaster()
  let attempts = 0
  onDestroy(() => {
    toast.dismiss()
  })

  function save() {
    const succeeds = ++attempts % 2 === 1
    return new Promise<void>((resolve, reject) =>
      setTimeout(
        () => (succeeds ? resolve() : reject(new Error("Network error"))),
        1500
      )
    )
  }

  function saveChanges() {
    toast.promise(save, {
      loading: "Saving changes…",
      success: "Changes saved",
      error: {
        title: "Couldn't save changes",
        description: "Check your connection and try again.",
        action: { label: "Retry", onAction: saveChanges },
      },
    })
  }
</script>

<button onclick={saveChanges}>Save Changes</button>
<ToastRegion toaster={toast}>
  <Toast>
    <div data-slot="stack">
      <ToastTitle />
      <ToastDescription />
    </div>
    <ToastAction data-variant="solid" />
  </Toast>
</ToastRegion>
Press Save Changes to see Saving changes become Changes saved. Press it again for the error, then choose Retry.

Stacking

maxVisible limits how many toasts show at once, three by default; the rest wait in the queue, and their timers start when they show. The stack expands while the pointer or focus is in it. Set expanded to keep it open. Styling shows the CSS that lays out the stack.

<script lang="ts">
  import { onDestroy } from "svelte"
  import { CheckIcon } from "@lucide/svelte"
  import { Checkbox, CheckboxIndicator } from "@ui-sdk/svelte/checkbox"
  import {
    Toast,
    ToastDescription,
    ToastRegion,
    ToastTitle,
    createToaster,
  } from "@ui-sdk/svelte/toast"

  const notifications = [
    { title: "Event created", description: "Friday, June 12 at 10:00 AM" },
    { title: "Message sent" },
    {
      title: "Hollis Grant joined",
      description: "They can now edit the Launch plan and see its comments.",
    },
    { title: "Changes saved" },
    { title: "Invoice sent", description: "Inès Moreau · $240.00" },
    { title: "Photos uploaded", description: "12 photos added to Summer trip" },
  ]

  const toast = createToaster()
  let expanded = $state(false)
  let count = 0
  onDestroy(() => {
    toast.dismiss()
  })
</script>

<div data-slot="stack">
  <div data-slot="controls">
    <label data-slot="label">
      <Checkbox
        checked={expanded}
        onCheckedChange={(next) => (expanded = next === true)}
      >
        <CheckboxIndicator>
          <CheckIcon aria-hidden="true" />
        </CheckboxIndicator>
      </Checkbox>
      Always expanded
    </label>
  </div>
  <button onclick={() => toast(notifications[count++ % notifications.length])}
    >Add Toast</button
  >
  <ToastRegion toaster={toast} maxVisible={4} {expanded}>
    <Toast>
      <div data-slot="stack">
        <ToastTitle />
        <ToastDescription />
      </div>
    </Toast>
  </ToastRegion>
</div>
Press Add Toast a few times, then hover the stack to expand it. Past four toasts, the rest wait their turn. Check Always expanded to keep it open.

Duration

Set duration in milliseconds, on the toaster or on one toast, or Infinity for a toast that stays until it's dismissed. The default is five seconds. Timers pause while the pointer or focus is in the region, and while the page is hidden or its window is inactive.

<script lang="ts">
  import { onDestroy } from "svelte"
  import {
    Toast,
    ToastClose,
    ToastRegion,
    ToastTitle,
    createToaster,
  } from "@ui-sdk/svelte/toast"

  const toast = createToaster()
  onDestroy(() => {
    toast.dismiss()
  })
</script>

<div data-slot="row">
  <button onclick={() => toast("Link copied", { duration: 1000 })}
    >1 Second</button
  >
  <button onclick={() => toast("Changes saved")}>5 Seconds</button>
  <button
    onclick={() =>
      toast("Your session expires in 5 minutes", {
        duration: Infinity,
        dismissible: false,
        cancel: { label: "Got It" },
      })}
  >
    Until Dismissed
  </button>
</div>
<ToastRegion toaster={toast}>
  {#snippet children(item)}
    <Toast>
      <ToastTitle />
      {#if item.cancel}
        <ToastClose data-variant="solid" action={item.cancel.onCancel}>
          {item.cancel.label}
        </ToastClose>
      {/if}
    </Toast>
  {/snippet}
</ToastRegion>
Press each button. Hover a toast to pause its timer. The session toast stays until you press Got It.

Update and Dismiss

Keep the id toast() returns to update or dismiss that toast, or pass your own id. toast.update() returns whether the toast was still there, and toast.dismiss() without an id closes every toast.

Console
<script lang="ts">
  import { onDestroy } from "svelte"
  import { CircleCheckIcon, LoaderCircleIcon } from "@lucide/svelte"
  import {
    Toast,
    ToastRegion,
    ToastTitle,
    createToaster,
  } from "@ui-sdk/svelte/toast"
  import { useLog } from "./log"

  const log = useLog()
  const toast = createToaster()
  let timer: ReturnType<typeof setInterval> | undefined
  onDestroy(() => {
    clearInterval(timer)
    toast.dismiss()
  })

  function upload() {
    clearInterval(timer)
    const id = toast.loading("Uploading report.pdf… 0%", { id: "upload" })
    let progress = 0
    timer = setInterval(() => {
      progress += 20
      const done = progress >= 100
      const open = toast.update(
        id,
        done
          ? { title: "Report uploaded", type: "success" }
          : { title: `Uploading report.pdf… ${progress}%` }
      )
      if (!open) {
        log("Upload canceled")
      }
      if (done || !open) {
        clearInterval(timer)
      }
    }, 700)
  }
</script>

<div data-slot="row">
  <button onclick={upload}>Upload Report</button>
  <button onclick={() => toast.dismiss("upload")}>Cancel Upload</button>
  <button onclick={() => toast.dismiss()}>Dismiss All</button>
</div>
<ToastRegion toaster={toast}>
  <Toast>
    <CircleCheckIcon data-show="success" aria-hidden="true" />
    <LoaderCircleIcon data-show="loading" aria-hidden="true" />
    <ToastTitle />
  </Toast>
</ToastRegion>
Press Upload Report and watch one toast count up to Report uploaded. Cancel Upload closes it early, and Dismiss All clears every toast.

Async Action

Return a promise from an action to keep the toast open while it runs. The action's state has pending while it runs and error if it fails, so the button can say Restoring… or offer a retry, and presses are ignored until it settles. See Button for changes made after an await.

Console
<script lang="ts">
  import { onDestroy } from "svelte"
  import {
    Toast,
    ToastAction,
    ToastDescription,
    ToastRegion,
    ToastTitle,
    createToaster,
  } from "@ui-sdk/svelte/toast"
  import { useLog } from "./log"

  function restore() {
    return new Promise((resolve) => setTimeout(resolve, 1500))
  }

  const log = useLog()
  const toast = createToaster({ duration: 6000 })
  onDestroy(() => {
    toast.dismiss()
  })
</script>

<button
  onclick={() =>
    toast("File deleted", {
      description: "Q3 report.pdf",
      action: {
        label: "Undo",
        onAction: async () => {
          await restore()
          log("Q3 report.pdf restored")
        },
      },
    })}
>
  Delete File
</button>
<ToastRegion toaster={toast}>
  <Toast>
    <div data-slot="stack">
      <ToastTitle />
      <ToastDescription />
    </div>
    <ToastAction data-variant="solid">
      {#snippet children(state)}
        {state.pending ? "Restoring…" : state.error ? "Retry" : "Undo"}
      {/snippet}
    </ToastAction>
  </Toast>
</ToastRegion>
Press Delete File, then Undo. The button shows Restoring… until the restore finishes, then the toast closes.

Keyboard Shortcut

F6 moves focus to the newest toast and back. Set the region's shortcut to use another key, or null to turn it off. The region advertises it through aria-keyshortcuts.

Console
<script lang="ts">
  import { onDestroy } from "svelte"
  import {
    Toast,
    ToastAction,
    ToastRegion,
    ToastTitle,
    createToaster,
  } from "@ui-sdk/svelte/toast"
  import { useLog } from "./log"

  const log = useLog()
  const toast = createToaster({ duration: 10000 })
  onDestroy(() => {
    toast.dismiss()
  })
</script>

<button
  onclick={() =>
    toast("Message sent", {
      action: { label: "Undo", onAction: () => log("Message unsent") },
    })}
>
  Send Message
</button>
<ToastRegion toaster={toast} shortcut="alt+t" aria-label="Alerts">
  <Toast>
    <ToastTitle />
    <ToastAction data-variant="solid" />
  </Toast>
</ToastRegion>
Press Send Message, then Alt+T (Option+T on a Mac) to focus the toast. Press Escape to dismiss it, or Alt+T again to go back.

Overlay Host and Confirm

Instead of placing a region, render OverlayHost once near your app's root. It renders the toaster's region, with the toast.children snippet you give it, and confirm() questions as alert dialogs. A region you place yourself takes over from the host for its toaster.

To style the question, render an AlertDialog with the confirm controller and read the question from its content's payload. Alert Dialog covers createConfirm() in more detail.

<script lang="ts">
  import { onDestroy } from "svelte"
  import { CircleCheckIcon } from "@lucide/svelte"
  import {
    AlertDialog,
    AlertDialogBackdrop,
    AlertDialogClose,
    AlertDialogContent,
    AlertDialogDescription,
    AlertDialogTitle,
    createConfirm,
    type ConfirmOptions,
  } from "@ui-sdk/svelte/alert-dialog"
  import type { DialogContentState } from "@ui-sdk/svelte/dialog"
  import { OverlayHost, createOverlays } from "@ui-sdk/svelte/overlays"
  import { Toast, ToastTitle, createToaster } from "@ui-sdk/svelte/toast"

  const overlays = createOverlays()
  const toast = createToaster({ overlays })
  const confirm = createConfirm({ overlays })
  let projects = $state(["Acme", "Globex", "Initech"])
  let reset: HTMLButtonElement
  onDestroy(() => {
    toast.dismiss()
  })

  async function remove(name: string, button: HTMLButtonElement) {
    const confirmed = await confirm({
      title: `Delete ${name}?`,
      description:
        "The project and its files will be deleted for everyone on your team.",
      confirmLabel: "Delete",
      cancelLabel: "Cancel",
    })
    if (!confirmed) {
      return
    }
    const row = button.closest("li")
    const next = (
      row?.nextElementSibling ?? row?.previousElementSibling
    )?.querySelector("button")
    ;(next ?? reset)?.focus()
    projects = projects.filter((project) => project !== name)
    toast.success(`${name} deleted`, { priority: "assertive" })
  }
</script>

{#snippet toastItem()}
  <Toast>
    <CircleCheckIcon data-show="success" aria-hidden="true" />
    <ToastTitle />
  </Toast>
{/snippet}

<div data-slot="stack">
  <div data-slot="controls">
    <button
      type="button"
      bind:this={reset}
      onclick={() => (projects = ["Acme", "Globex", "Initech"])}
    >
      Reset Projects
    </button>
  </div>
  <ul data-slot="list" aria-label="Projects">
    {#each projects as name (name)}
      <li data-slot="row">
        {name}
        <button
          type="button"
          data-size="sm"
          aria-label={`Delete ${name}`}
          onclick={(event) => remove(name, event.currentTarget)}
        >
          Delete
        </button>
      </li>
    {:else}
      <li data-slot="row">All projects deleted.</li>
    {/each}
  </ul>
  <OverlayHost {overlays} toast={{ children: toastItem }} />
  <AlertDialog controller={confirm.controller}>
    <AlertDialogBackdrop />
    <AlertDialogContent>
      {#snippet children(state: DialogContentState<ConfirmOptions>)}
        <div data-slot="stack">
          <AlertDialogTitle>{state.payload?.title}</AlertDialogTitle>
          <AlertDialogDescription
            >{state.payload?.description}</AlertDialogDescription
          >
        </div>
        <div data-slot="row">
          <AlertDialogClose>{state.payload?.cancelLabel}</AlertDialogClose>
          <AlertDialogClose data-variant="solid" value="confirm">
            {state.payload?.confirmLabel}
          </AlertDialogClose>
        </div>
      {/snippet}
    </AlertDialogContent>
  </AlertDialog>
</div>
Press Delete on a project and confirm. The row goes away and a toast says so. Reset Projects brings them back.

Custom Rendering

Pass anything else your toast needs in data, and read it from the toast the region's children snippet receives. Every part also takes a render snippet; this example renders each toast's li itself, making the ones hidden behind a collapsed stack inert.

Console
<script lang="ts">
  import { onDestroy } from "svelte"
  import { XIcon } from "@lucide/svelte"
  import {
    Toast,
    ToastAction,
    ToastClose,
    ToastDescription,
    ToastRegion,
    ToastTitle,
    createToaster,
  } from "@ui-sdk/svelte/toast"
  import { useLog } from "./log"

  const log = useLog()
  const toast = createToaster()
  onDestroy(() => {
    toast.dismiss()
  })
</script>

<button
  onclick={() =>
    toast({
      title: "Hollis Grant",
      description: "Can you review the draft?",
      data: { initials: "HG" },
      action: {
        label: "Reply",
        onAction: () => log("Replying to Hollis Grant"),
      },
    })}
>
  New Message
</button>
<ToastRegion toaster={toast} aria-label="Messages">
  {#snippet children(item)}
    <Toast>
      {#snippet render({ props, state, children })}
        <li
          {...props}
          inert={state.leaving ||
            (!state.expanded && state.index > 0) ||
            undefined}
        >
          {@render children?.(state)}
        </li>
      {/snippet}
      <span data-slot="media"
        >{(item.data as { initials: string }).initials}</span
      >
      <div data-slot="stack">
        <ToastTitle />
        <ToastDescription />
      </div>
      <ToastAction data-variant="solid" />
      <ToastClose aria-label="Close">
        <XIcon aria-hidden="true" />
      </ToastClose>
    </Toast>
  {/snippet}
</ToastRegion>
Press New Message to show a toast with Hollis Grant's initials. Choose Reply or close it.

Styling

Toasts are unstyled, and the region and its toasts carry what you need to animate a stack:

  • The stack. The region sets --toast-front-height, the newest toast's height, and --toast-stack-height, all of them added up. Each toast gets --toast-index, 0 for the newest, --toast-offset, the height of the toasts in front of it, and --toast-height, its own height. Position the toasts absolutely in the region and set height: var(--toast-height) so changes in height animate.
  • Collapsed and expanded. data-front marks the newest toast. While the stack is collapsed, clip the toasts behind it to --toast-front-height and peek them out by their index. While it's expanded, the region and its toasts get data-expanded; move each toast by its --toast-offset.
  • Swiping. While a toast is swiped it gets data-swiping, and --toast-swipe-x and --toast-swipe-y hold how far it moved. Translate it by them and turn off transitions. When a swipe dismisses it, data-swipe-direction says which way, so it can leave in that direction. A mouse drag that starts on selectable text selects it, so set user-select: none on toasts to swipe them from anywhere.
  • Entering and leaving. A toast has data-starting-style for its first frame and data-ending-style while it leaves, and stays mounted until its transitions finish.

A stack at the bottom of the page, in plain CSS:

[data-slot="toast-region"] {
  width: 356px;
  height: var(--toast-front-height);
}

[data-slot="toast-region"][data-expanded] {
  height: calc(var(--toast-stack-height) + (var(--toast-count) - 1) * 12px);
}

[data-slot="toast"] {
  --y: calc(var(--toast-index) * -12px);
  position: absolute;
  inset-inline: 0;
  bottom: 0;
  z-index: calc(100 - var(--toast-index));
  overflow: hidden;
  height: var(--toast-height);
  translate: var(--toast-swipe-x, 0px)
    calc(var(--y) + var(--toast-swipe-y, 0px));
  scale: calc(1 - var(--toast-index) * 0.05);
  transition-property: translate, scale, opacity, height;
  transition-duration: 400ms;
  user-select: none;
}

[data-slot="toast"]:not([data-front], [data-expanded]) {
  height: var(--toast-front-height);
}

[data-slot="toast"][data-expanded] {
  --y: calc(-1 * (var(--toast-offset) + var(--toast-index) * 12px));
  scale: 1;
}

[data-slot="toast"][data-starting-style] {
  transition: none;
}

[data-slot="toast"][data-starting-style][data-front] {
  --y: 100%;
}

[data-slot="toast"]:is(
  [data-starting-style]:not([data-front]),
  [data-ending-style]
) {
  opacity: 0;
}

[data-slot="toast"][data-swiping] {
  transition: none;
}

@media (prefers-reduced-motion: reduce) {
  [data-slot="toast"] {
    transition-property: opacity;
  }
}

Each part has a data-slot, such as toast, and its state attributes are listed under Data Attributes.

ToastRegion

VariableWhat it holds
--toast-front-heightThe newest toast's height. Clip the toasts behind it to it in a collapsed stack.
--toast-stack-heightThe toasts' heights added up, for the height of an expanded stack.
--toast-countHow many toasts show.
--keyboard-insetHow many pixels of the page the on-screen keyboard covers, 0 without one.
--visual-viewport-widthThe width of the part of the page you can see.
--visual-viewport-heightThe height of the part of the page you can see, above the keyboard.
--visual-viewport-topHow far the part of the page you can see is scrolled from the top of the layout, as when the keyboard pushes it up.

Toast

VariableWhat it holds
--toast-indexThe toast's place in the stack, 0 for the newest. A closing toast keeps its place.
--toast-offsetThe height of the toasts in front of this one, in pixels. Add gaps with --toast-index to place an expanded stack.
--toast-heightThe toast's own height with its content laid out freely, whatever height it's given. Set it as the height to animate the stack.
--toast-swipe-xHow far a swipe moved the toast sideways, in pixels. Translate the toast by it.
--toast-swipe-yHow far a swipe moved the toast up or down, in pixels.

Data Attributes

ToastTitle and ToastDescription have no state attributes. ToastClose gets data-pending, like ToastAction, while a Promise from its action runs.

ToastRegion

[data-slot="toast-region"]

AttributeWhat it means
data-sidetopbottomThe edge of the viewport the region sits on.
data-alignstartcenterendWhere along that edge.
data-expandedPresent while the stack is expanded: hovered, focused, or always with expanded.
data-keyboardPresent on the region while the on-screen keyboard covers part of the page.

Toast

[data-slot="toast"]

AttributeWhat it means
data-typedefaultsuccesserrorinfowarningloadingWhat the toast is about.
data-frontPresent on the newest toast, in front of a collapsed stack. A closing toast keeps it while it animates out.
data-expandedPresent while the stack is expanded: hovered, focused, or always with expanded.
data-starting-stylePresent for a toast's first frame. Style the state you animate from.
data-ending-stylePresent while a toast animates out. Style the state you animate to.

ToastAction

[data-slot="toast-action"]

AttributeWhat it means
data-pendingPresent on an action or close button while its handler's Promise runs.

API Reference

Toast

id?string
Reuses a toast: a toast with this id that's still showing is updated instead of added again.
title?Content
The main message.
description?Content
More detail, under the title.
type?ToastType
What it's about, for styling through data-type, and for how it's announced.
duration?number
How long it shows, in ms, while nothing pauses it. Infinity keeps it until dismissed.
priority?"assertive" | "polite"
How screen readers announce it: "polite" waits for a pause, "assertive" interrupts.
action?ToastAction<Content>
A button that does something, like Undo.
cancel?ToastCancel<Content>
A button that dismisses it, like Cancel.
dismissible?boolean
Whether the user can dismiss it: swiping, Escape and Close parts. Timers and dismiss() still close it.
data?unknown
Anything else, for your own rendering, like an avatar.
onDismiss?((details: ToastDismissDetails) => void)
Runs when it closes, with why. Calling details.cancel() keeps it.

toast(message, options) and toast(options) return the toast's id. toast.success, .info, .warning, .error and .loading set the type. toast.promise(promise, { loading, success, error }) returns the id too, and toast.update(id, options) returns whether the toast was still there. toast.dismiss(id) closes one, and toast.dismiss() every one.

Actions

labelContent
The button's text.
onAction?((event: Event | undefined, details: ToastActionDetails) => unknown)
Runs when it's pressed. A returned Promise marks the action pending, then dismisses the toast once it resolves. A rejection keeps the toast. details.source says how it was pressed, for changes made after an await.
href?string
Makes the action a link.
dismiss?boolean
Whether pressing it dismisses the toast.
labelContent
The button's text.
onCancel?((event: Event | undefined, details: ToastActionDetails) => void)
Runs when it's pressed, before the toast closes. details.source says how it was pressed.

ToastRegion

toaster?Toaster<any>
The queue it shows. A second toaster, from createToaster(), gives a second region its own toasts.
side?"bottom" | "top"
The edge of the viewport it sits on. Swiping toward it dismisses a toast.
align?"center" | "end" | "start"
Where along that edge. "start" and "end" follow the writing direction, and swiping toward them dismisses too.
maxVisible?number
How many toasts show at once. Older ones wait, with their timers stopped, until there's room.
expanded?boolean
Whether the stack is always expanded. Otherwise it expands while the pointer or focus is in it.
shortcut?string | null
The key that moves focus into the region and back, like "F6" or "alt+t". null turns it off.
pendingAnnouncement?string
What screen readers hear once a toast's action or close button has been pending for a second, so a slow action doesn't pass in silence. Set it in the page's language. An empty string says nothing.
debug?boolean
Logs every state change with its reason to the console, in development builds.

ToastAction

onAction?((event: Event | undefined, details: ToastActionDetails) => unknown)
Runs when it's pressed, instead of the toast's own action.onAction. A returned Promise marks it pending. details.source says how it was pressed, for changes made after an await.
dismiss?boolean
Whether pressing it dismisses the toast.

ToastClose

action?((event: Event | undefined, details: ToastActionDetails) => unknown)
Runs before the toast closes. A returned Promise keeps it open and the button pending until it settles. details.source says how it was pressed, for changes made after an await.

Confirm

titlestring
The question, like "Delete this project?".
description?string
What agreeing does.
confirmLabel?string
The agreeing button's text.
cancelLabel?string
The other button's text.

State

ToastRegion:

side"top" | "bottom"
The edge it sits on.
align"start" | "center" | "end"
Where along that edge.
expandedboolean
Whether the stack is expanded.
toastsreadonly ToastItem<unknown>[]
The toasts to render, newest first, including ones animating out.

Toast:

typeToastType
What it's about.
indexnumber
Its place in the stack, 0 for the newest.
frontboolean
Whether it's the newest, on top of a collapsed stack. A closing toast keeps it.
expandedboolean
Whether the stack is expanded.
leavingboolean
Whether it's animating out.

ToastAction and ToastClose:

pendingboolean
Whether a Promise its handler returned is still running.
errorunknown
Why its handler failed, until it's pressed again.

Reasons

timeoutnone
A duration ran out, like a toast's, or a rotating carousel's interval.
close-pressMouseEvent | KeyboardEvent | SubmitEvent
A Close part was pressed, or a form with method="dialog" was submitted.
swipePointerEvent | TouchEvent
A swipe gesture: a finger, mouse or pen dragged it.
escape-keyKeyboardEvent
Escape, or a submenu's close key (ArrowLeft in left-to-right layouts).
actionMouseEvent | KeyboardEvent
A Close part's action finished.
imperativenone
A controller or api call, or a property set on a web component.

Accessibility

Toasts are announced through shared live regions, once each, without making the visible region live: politely by default and assertively for errors. While it has toasts, the region is a landmark named Notifications; set aria-label to name it in the page's language. Keep messages short, and use duration: Infinity for one that has to stay available.

Keyboard

F6, or the region's shortcut, moves focus to the newest toast, and pressing it again returns focus to where it came from. Tab moves through the toasts and their buttons, and Tab past the last one goes back too. Escape dismisses the focused toast; focus then moves to the next toast, or back where it came from after the last one. The region stays reachable while a modal dialog is open, and its timers pause while focus is inside.

Tested Behavior

Every line below is an automated test.

Showing
  • A toast shows its title, description and action in the Notifications region.
  • Newer toasts come first.
  • The newest toast is marked as the front one.
  • Each toast says what it's about through data-type.
  • The region sits on its side and alignment.
  • It shows above everything, in the top layer.
  • A promise toast loads, then shows the result.
  • A toast with an id that's showing is updated in place.
Closing
  • A toast closes after its duration.
  • Infinity keeps it until it's dismissed.
  • The Dismiss button closes it.
  • The action runs and closes it.
  • Escape on a focused toast closes it.
  • A closing toast stays while it animates out.
  • A toast that can't be dismissed ignores Escape.
Timers
  • Hovering the region pauses its timers, and leaving resumes them.
  • Focus in the region pauses its timers.
Keyboard
  • F6 moves focus to the newest toast, and again back where it was.
  • The shortcut is configurable.
  • The region names its shortcut.
  • When the focused toast closes, focus goes to the next one, then back where it was.
Stacking
  • Only maxVisible toasts show; the rest wait for room.
  • The stack expands while hovered or focused.
  • Expanded keeps the stack expanded.
  • Toasts measure their heights and offsets for the stack.
  • A toast measures its content's height, whatever height it's given.
  • A toast given its measured height animates to a new one when its content changes.
  • A toast measures again when a child, its class or its padding changes.
  • A toast closing from the front stays in front while it animates out.
Swiping
  • Swiping toward the region's side dismisses.
  • Swiping toward the aligned edge dismisses too.
  • A short swipe snaps back.
  • Swiping away from the side never dismisses.
  • A toast that can't be dismissed doesn't swipe.
Over a modal dialog
  • A toast shown from a modal dialog is visible, usable and above it.
Updates after closing
  • A promise toast dismissed while loading stays closed.
  • An update after a toast closed never brings it back.
More pausing and keys
  • The window losing focus pauses timers, and getting it back resumes them.
  • F6 does nothing while there are no toasts, so the browser keeps it.
  • A toast on the top edge swipes up, and a swipe down snaps back.

Runs in React, Vue and web components.

Also tested:

Enhancements
  • Update() says whether the toast was still there.
  • A toast beyond maxVisible never expires unseen.
  • There's no empty Notifications landmark.
  • Focus never falls to the page when a toast focused by the mouse closes.
  • A pending action shows it and holds the toast open until it settles.
Pausing
  • A hidden page pauses timers.
  • Pause reasons hold separately: getting the window back doesn't resume while focus is in the region.
Announcements
  • The live regions exist before the first toast.
  • A toast is announced politely, with its title and description.
  • An error is announced assertively.
  • Toasts that arrive together are announced together.
  • The visible region isn't a live region, so nothing is read twice.
The shortcut
  • F6 works from a text field and types nothing.
  • A shortcut of null turns it off.
  • Tab past the last toast goes back where F6 came from.
Over modal dialogs
  • F6 reaches the toasts from inside a modal dialog, and goes back into it.
  • Escape on a toast closes the toast, not the dialog beneath.
  • A dialog opened after a toast doesn't cover it, and pressing it doesn't close the dialog.
Swiping
  • Dragging across a toast's text with a mouse selects it instead of dismissing.

Runs in React, Vue and web components.

Screen readers hear the same thing in each tested framework, recorded by a virtual screen reader:

  • Announcements while reaching a toast with F6 and leaving it.

Runs in React, Vue and web components.