A toast built from the Base toast parts, with a toast manager you can call from anywhere.
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/baseCopy 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.
<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
)}
/><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)} /><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><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)} /><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)} /><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}<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}<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} /><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)} /><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)} /><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>import { Toast as Primitive } from '@sveltery/base/toast';
export const toast = Primitive.createToastManager();
export const createToastManager = Primitive.createToastManager;
export const getToastManager = Primitive.getToastManager;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.
| Prop | Type | Default | Description |
|---|---|---|---|
timeout | number | 5000 | Milliseconds before a toast closes itself. 0 keeps it open. |
limit | number | 3 | How many toasts show at once. Older toasts get data-limited. |
toastManager | ToastManager | toast | The manager that feeds this provider. |
toast
| Method | Description |
|---|---|
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.
| Option | Type | Description |
|---|---|---|
id | string | Reuse an id to update that toast in place. |
title | string \| Snippet | The first line. |
description | string \| Snippet | The second line. |
type | string | Picks the icon and sets data-type. |
timeout | number | Milliseconds before this toast closes itself. 0 keeps it open. |
priority | 'low' \| 'high' | high is announced urgently. |
actionProps | ButtonHTMLAttributes | Props for the action button. children is its label. |
onClose | () => void | Runs when the toast closes. |
onRemove | () => void | Runs when the toast leaves the DOM after its exit animation. |
data | object | Custom data for your own layout. |
Parts
Each part accepts the props of the Base part it wraps and sets a data-slot.
| Component | Base part | data-slot |
|---|---|---|
ToastProvider | Toast.Provider | none |
ToastPortal | Toast.Portal | toast-portal |
ToastViewport | Toast.Viewport | toast-viewport |
Toast | Toast.Root | toast |
ToastContent | Toast.Content | toast-content |
ToastTitle | Toast.Title | toast-title |
ToastDescription | Toast.Description | toast-description |
ToastAction | Toast.Action | toast-action |
ToastClose | Toast.Close | toast-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.