A toast built from the Base toast parts, with a toast manager you can call from anywhere.

Basic
With Action
Promise

About

Toast is the shadcn/ui Toast, built on the Sveltery Base Toast parts. Toaster renders the provider, the portal, the viewport and one Toast for each toast. The toast manager adds toasts from components, modules and event handlers. Sveltery UI has no Sonner component, because Toast covers the same behavior. Where shadcn/ui code calls toast() from sonner, call toast.add() here.

Installation

Install the dependencies

pnpm add cn class-variance-authority github:sveltery/base

Copy the component

Copy the following files into src/lib/components/ui/toast. The toast action and close button also need the Button component's buttonVariants.

It imports the button component, so copy its folder too.

src/lib/components/ui/toast/Toast.svelte
<script lang="ts">
  import type { ComponentProps } from 'svelte';
  import { Toast as Primitive } from '@sveltery/base/toast';
  import { cn } from 'cn';
  let { children, class: classProp, ...props }: ComponentProps<typeof Primitive.Root> = $props();
</script>
<Primitive.Root
  data-slot="toast"
  {...props}
  {children}
  class={cn(
    'rounded-2xl group/toast pointer-events-auto absolute right-0 bottom-0 z-[calc(1000-var(--toast-index))] w-full origin-bottom border bg-popover text-popover-foreground shadow-lg will-change-transform outline-none select-none focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50',
    '[--gap:0.75rem] [--height:var(--toast-frontmost-height,var(--toast-height))] [--offset-y:calc(var(--toast-offset-y)*-1+calc(var(--toast-index)*var(--gap)*-1)+var(--toast-swipe-movement-y))] [--peek:0.75rem] [--scale:calc(max(0,1-(var(--toast-index)*0.1)))] [--shrink:calc(1-var(--scale))]',
    'h-(--height) [transform:translateX(var(--toast-swipe-movement-x))_translateY(calc(var(--toast-swipe-movement-y)-(var(--toast-index)*var(--peek))-(var(--shrink)*var(--height))))_scale(var(--scale))] [transition:transform_500ms_cubic-bezier(0.22,1,0.36,1),opacity_500ms,height_150ms]',
    "after:absolute after:top-full after:left-0 after:h-[calc(var(--gap)+1px)] after:w-full after:content-['']",
    'data-expanded:h-(--toast-height) data-expanded:[transform:translateX(var(--toast-swipe-movement-x))_translateY(var(--offset-y))]',
    'data-limited:opacity-0 data-starting-style:[transform:translateY(150%)]',
    '[&[data-ending-style]:not([data-limited]):not([data-swipe-direction])]:[transform:translateY(150%)]',
    'data-ending-style:data-[swipe-direction=down]:[transform:translateY(calc(var(--toast-swipe-movement-y)+150%))]',
    'data-ending-style:data-[swipe-direction=left]:[transform:translateX(calc(var(--toast-swipe-movement-x)-150%))_translateY(var(--offset-y))]',
    'data-ending-style:data-[swipe-direction=right]:[transform:translateX(calc(var(--toast-swipe-movement-x)+150%))_translateY(var(--offset-y))]',
    'data-ending-style:data-[swipe-direction=up]:[transform:translateY(calc(var(--toast-swipe-movement-y)-150%))]',
    'data-expanded:data-ending-style:data-[swipe-direction=down]:[transform:translateY(calc(var(--toast-swipe-movement-y)+150%))]',
    'data-expanded:data-ending-style:data-[swipe-direction=left]:[transform:translateX(calc(var(--toast-swipe-movement-x)-150%))_translateY(var(--offset-y))]',
    'data-expanded:data-ending-style:data-[swipe-direction=right]:[transform:translateX(calc(var(--toast-swipe-movement-x)+150%))_translateY(var(--offset-y))]',
    'data-expanded:data-ending-style:data-[swipe-direction=up]:[transform:translateY(calc(var(--toast-swipe-movement-y)-150%))]',
    classProp
  )}
/>
src/lib/components/ui/toast/ToastAction.svelte
<script lang="ts">
  import type { ComponentProps, Snippet } from 'svelte';
  import type { HTMLButtonAttributes } from 'svelte/elements';
  import { Toast as Primitive } from '@sveltery/base/toast';
  import { cn } from 'cn';
  import { Button } from '../button/index.js';
  let { class: classProp, render, ...props }: ComponentProps<typeof Primitive.Action> = $props();
</script>
{#snippet button(buttonProps: HTMLButtonAttributes, _state: unknown, content: Snippet | undefined)}
  <Button variant="outline" size="sm" {...buttonProps}>{@render content?.()}</Button>
{/snippet}
<Primitive.Action data-slot="toast-action" render={render ?? button} {...props} class={cn('shrink-0', classProp)} />
src/lib/components/ui/toast/ToastClose.svelte
<script lang="ts">
  import type { ComponentProps, Snippet } from 'svelte';
  import type { HTMLButtonAttributes } from 'svelte/elements';
  import { Toast as Primitive } from '@sveltery/base/toast';
  import { cn } from 'cn';
  import { Button } from '../button/index.js';
  import XIcon from '@lucide/svelte/icons/x';
  let { children, class: classProp, render, ...props }: ComponentProps<typeof Primitive.Close> = $props();
</script>
{#snippet button(buttonProps: HTMLButtonAttributes, _state: unknown, content: Snippet | undefined)}
  <Button variant="ghost" size="icon-sm" {...buttonProps}>{@render content?.()}</Button>
{/snippet}
<Primitive.Close data-slot="toast-close" aria-label="Close toast" render={render ?? button} {...props} class={cn("relative shrink-0 text-muted-foreground after:absolute after:-inset-2 after:content-[''] hover:text-foreground", classProp)}>
  {#if children}
    {@render children()}
  {:else}
    <XIcon aria-hidden="true" />
  {/if}
</Primitive.Close>
src/lib/components/ui/toast/ToastContent.svelte
<script lang="ts">
  import type { ComponentProps } from 'svelte';
  import { Toast as Primitive } from '@sveltery/base/toast';
  import { cn } from 'cn';
  let { children, class: classProp, ...props }: ComponentProps<typeof Primitive.Content> = $props();
</script>
<Primitive.Content data-slot="toast-content" {...props} {children} class={cn('flex h-full items-center gap-3 overflow-hidden p-4 transition-opacity duration-250 ease-[cubic-bezier(0.22,1,0.36,1)] data-behind:opacity-0 data-expanded:opacity-100', classProp)} />
src/lib/components/ui/toast/ToastDescription.svelte
<script lang="ts">
  import type { ComponentProps } from 'svelte';
  import { Toast as Primitive } from '@sveltery/base/toast';
  import { cn } from 'cn';
  let { children, class: classProp, ...props }: ComponentProps<typeof Primitive.Description> = $props();
</script>
<Primitive.Description data-slot="toast-description" {...props} {children} class={cn('text-sm text-muted-foreground', classProp)} />
src/lib/components/ui/toast/ToastIcon.svelte
<script lang="ts">
  import CircleCheckIcon from '@lucide/svelte/icons/circle-check';
  import InfoIcon from '@lucide/svelte/icons/info';
  import TriangleAlertIcon from '@lucide/svelte/icons/triangle-alert';
  import OctagonXIcon from '@lucide/svelte/icons/octagon-x';
  import Loader2Icon from '@lucide/svelte/icons/loader-2';
  let { type }: { type: string | undefined } = $props();
</script>
{#if type === 'success' || type === 'info' || type === 'warning' || type === 'error' || type === 'loading'}
  <span data-slot="toast-icon" class="shrink-0 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4">
    {#if type === 'success'}
      <CircleCheckIcon aria-hidden="true" />
    {:else if type === 'info'}
      <InfoIcon aria-hidden="true" />
    {:else if type === 'warning'}
      <TriangleAlertIcon aria-hidden="true" />
    {:else if type === 'error'}
      <OctagonXIcon class="text-destructive" aria-hidden="true" />
    {:else}
      <Loader2Icon class="animate-spin" aria-hidden="true" />
    {/if}
  </span>
{/if}
src/lib/components/ui/toast/ToastList.svelte
<script lang="ts">
  import { getToastManager } from './toast.js';
  import Toast from './Toast.svelte';
  import ToastAction from './ToastAction.svelte';
  import ToastClose from './ToastClose.svelte';
  import ToastContent from './ToastContent.svelte';
  import ToastDescription from './ToastDescription.svelte';
  import ToastIcon from './ToastIcon.svelte';
  import ToastTitle from './ToastTitle.svelte';
  const manager = getToastManager();
</script>
{#each manager.toasts as toastItem (toastItem.id)}
  <Toast toast={toastItem}>
    <ToastContent>
      <ToastIcon type={toastItem.type} />
      <div class="flex min-w-0 flex-1 flex-col gap-1">
        <ToastTitle />
        <ToastDescription />
      </div>
      <ToastAction />
      <ToastClose />
    </ToastContent>
  </Toast>
{/each}
src/lib/components/ui/toast/ToastPortal.svelte
<script lang="ts">
  import type { ComponentProps } from 'svelte';
  import { Toast as Primitive } from '@sveltery/base/toast';
  let { children, ...props }: ComponentProps<typeof Primitive.Portal> = $props();
</script>
<Primitive.Portal data-slot="toast-portal" {...props} {children} />
src/lib/components/ui/toast/ToastTitle.svelte
<script lang="ts">
  import type { ComponentProps } from 'svelte';
  import { Toast as Primitive } from '@sveltery/base/toast';
  import { cn } from 'cn';
  let { children, class: classProp, ...props }: ComponentProps<typeof Primitive.Title> = $props();
</script>
<Primitive.Title data-slot="toast-title" {...props} {children} class={cn('text-sm font-medium', classProp)} />
src/lib/components/ui/toast/ToastViewport.svelte
<script lang="ts">
  import type { ComponentProps } from 'svelte';
  import { Toast as Primitive } from '@sveltery/base/toast';
  import { cn } from 'cn';
  let { children, class: classProp, ...props }: ComponentProps<typeof Primitive.Viewport> = $props();
</script>
<Primitive.Viewport data-slot="toast-viewport" {...props} {children} class={cn('pointer-events-none fixed inset-x-4 bottom-4 z-50 mx-auto w-auto max-w-sm outline-none sm:right-4 sm:left-auto sm:mx-0 sm:w-full', classProp)} />
src/lib/components/ui/toast/Toaster.svelte
<script lang="ts">
  import type { ComponentProps } from 'svelte';
  import { Toast as Primitive } from '@sveltery/base/toast';
  import { toast } from './toast.js';
  import ToastList from './ToastList.svelte';
  import ToastPortal from './ToastPortal.svelte';
  import ToastViewport from './ToastViewport.svelte';
  let { children, toastManager = toast, ...props }: ComponentProps<typeof Primitive.Provider> = $props();
</script>
<Primitive.Provider {toastManager} {...props}>
  {@render children?.()}
  <ToastPortal>
    <ToastViewport>
      <ToastList />
    </ToastViewport>
  </ToastPortal>
</Primitive.Provider>
src/lib/components/ui/toast/toast.ts
import { Toast as Primitive } from '@sveltery/base/toast';

export const toast = Primitive.createToastManager();
export const createToastManager = Primitive.createToastManager;
export const getToastManager = Primitive.getToastManager;
src/lib/components/ui/toast/index.ts
import { Toast as Primitive } from '@sveltery/base/toast';

export const ToastProvider = Primitive.Provider;
export { default as Toaster } from './Toaster.svelte';
export { default as Toast } from './Toast.svelte';
export { default as ToastPortal } from './ToastPortal.svelte';
export { default as ToastViewport } from './ToastViewport.svelte';
export { default as ToastContent } from './ToastContent.svelte';
export { default as ToastTitle } from './ToastTitle.svelte';
export { default as ToastDescription } from './ToastDescription.svelte';
export { default as ToastAction } from './ToastAction.svelte';
export { default as ToastClose } from './ToastClose.svelte';
export { toast, createToastManager, getToastManager } from './toast.js';

Update the import paths

Change the imports to match your project setup.

Usage

Mount Toaster once, in your root layout.

<script lang="ts">
  import { Toaster } from '$lib/components/ui/toast';
  let { children } = $props();
</script>

{@render children()}
<Toaster />

Call toast.add() from any component or module.

<script lang="ts">
  import { toast } from '$lib/components/ui/toast';
</script>

<button onclick={() => toast.add({ title: 'Event created', description: 'Sunday, December 3 at 9:00 AM' })}>
  Show Toast
</button>

Examples

With action

actionProps adds a button. children is its label and onclick runs when it is clicked. toast.add() returns the toast's id, so the handler can close the toast.

const id = toast.add({
  title: 'Event created',
  description: 'You can undo this action.',
  actionProps: {
    children: 'Undo',
    onclick() {
      toast.close(id);
      toast.add({ description: 'Event creation undone.' });
    }
  }
});

Promise

toast.promise() shows the loading toast, then replaces it with success or error. Each takes a string, an options object, or a function of the result or the error that returns either. It returns the promise you passed.

toast.promise(createEvent(), {
  loading: 'Creating event…',
  success: (data) => `${data.name} created.`,
  error: 'Could not create event.'
});

Types

Set type to success, info, warning, error or loading to show that icon. Other values show no icon.

toast.add({ title: 'Event created', type: 'success' });

Custom layout

Toaster is the standard layout. To change it, compose the parts yourself and read the toasts with getToastManager() inside a ToastProvider.

<script lang="ts">
  import { Toast, ToastClose, ToastContent, ToastDescription, ToastPortal, ToastTitle, ToastViewport, getToastManager } from '$lib/components/ui/toast';

  const manager = getToastManager();
</script>

<ToastPortal>
  <ToastViewport>
    {#each manager.toasts as item (item.id)}
      <Toast toast={item}>
        <ToastContent>
          <div class="flex min-w-0 flex-1 flex-col gap-1">
            <ToastTitle />
            <ToastDescription />
          </div>
          <ToastClose />
        </ToastContent>
      </Toast>
    {/each}
  </ToastViewport>
</ToastPortal>

Render that component inside <ToastProvider toastManager={toast}>.

API reference

Toaster

Toaster takes the Base Toast.Provider props and passes the global toast manager as toastManager. Pass your own manager from createToastManager() to use a separate stack.

PropTypeDefaultDescription
timeoutnumber5000Milliseconds before a toast closes itself. 0 keeps it open.
limitnumber3How many toasts show at once. Older toasts get data-limited.
toastManagerToastManagertoastThe manager that feeds this provider.

toast

MethodDescription
toast.add(options)Adds a toast and returns its id. An existing id updates that toast.
toast.update(id, updates)Changes a toast. updates is an object or a function of the current toast.
toast.close(id?)Closes one toast, or every toast without an id.
toast.promise(promise, opts)Shows a toast that follows the promise.

title and description are a string or a Svelte snippet.

OptionTypeDescription
idstringReuse an id to update that toast in place.
titlestring \| SnippetThe first line.
descriptionstring \| SnippetThe second line.
typestringPicks the icon and sets data-type.
timeoutnumberMilliseconds before this toast closes itself. 0 keeps it open.
priority'low' \| 'high'high is announced urgently.
actionPropsButtonHTMLAttributesProps for the action button. children is its label.
onClose() => voidRuns when the toast closes.
onRemove() => voidRuns when the toast leaves the DOM after its exit animation.
dataobjectCustom data for your own layout.

Parts

Each part accepts the props of the Base part it wraps and sets a data-slot.

ComponentBase partdata-slot
ToastProviderToast.Providernone
ToastPortalToast.Portaltoast-portal
ToastViewportToast.Viewporttoast-viewport
ToastToast.Roottoast
ToastContentToast.Contenttoast-content
ToastTitleToast.Titletoast-title
ToastDescriptionToast.Descriptiontoast-description
ToastActionToast.Actiontoast-action
ToastCloseToast.Closetoast-close

ToastClose shows an X icon unless you pass children. The icon for a toast's type has data-slot="toast-icon".

See the Base Toast documentation for the behavior it ports: stacking, hover and F6 expansion, swipe to dismiss and screen reader announcements.