Open-source project
wobsoriano/svelte-sonner avatar
wobsoriano/svelte-sonner

svelte-sonner brings Sonner's toast API to Svelte 5

An opinionated toast component for Svelte. A port of @emilkowalski's sonner.

1,298 stars44 forksSvelteMIT

At a glance

What is it?
A Svelte 5 port of Emil Kowalski's Sonner, with promise toasts, action buttons, a headless mode for Tailwind users, icon slots written as Svelte 5 snippets, and a documented prop set for theme, position and duration.
Who is it for?
Adopt svelte-sonner if you are on Svelte 5 and want Sonner's behaviour and API, and if the default styling is close enough to what you want. Do not adopt it if you are still on Svelte 4, since the peer dependency asks for svelte ^5.0.0, or if your design system requires full control of the markup, because the unstyled mode and the headless variant still keep Sonner's structure and behaviour.
Can I use it commercially?
Yes. MIT is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
Is it still maintained?
Yes. The repository last received commits 70 days ago.
What is it written in?
Mainly Svelte, according to GitHub's language statistics.

Answers come from the project's GitHub data, last synced on October 23, 2026, and from our analysis. They are not legal advice.

Editorial analysis

What the project is, and what it is a port of

svelte-sonner is described as an opinionated toast component for Svelte and a port of Emil Kowalski's Sonner. That second half is the important part of the description, because it fixes the design. You are not choosing a notification library in the abstract, you are taking a specific library's behaviour, layout and API and running it in another framework, which means the questions that matter are about fidelity and drift rather than about notification design in general.

The packaging metadata makes the technical constraints explicit. The peer dependency is svelte ^5.0.0, so the library is Svelte 5 only, and there is one runtime dependency, runed ^0.28.0. There is no CSS framework or icon package in the dependency list, which explains the shape of the styling API further down: everything visual is either the built in look, a class map you pass in, or a headless component you write yourself.

The project is MIT licensed, at version 1.2.1 in package.json, and the release tags line up with that, with v1.2.1 and v1.2.0 both published on 2026-08-14 and v1.1.1 on 2026-04-24. The last push to the repository was on 2026-08-14 as well, so the published version and the head of the branch are the same point in time. The project home is at svelte-sonner.vercel.app, and releases are managed with changesets rather than by hand editing the version.

Installing it and rendering the first toast

Installation is a single package manager command, and the README lists the three usual options:

bash
npm i svelte-sonner
# or
yarn add svelte-sonner
# or
pnpm add svelte-sonner

The usage model has two parts. A Toaster component is mounted once in the app, and it is the place where every toast is rendered. After that, the toast function can be called from anywhere in the app, including from outside a component:

svelte
<script>
	import { Toaster, toast } from 'svelte-sonner';
</script>

<Toaster />
<button onclick={() => toast('My first toast')}>Give me a toast</button>

That split is the design that makes the library pleasant to use in an application with many layers. Nothing has to be threaded through props or provided by context to raise a notification, because the toast function is a plain import. The cost is that the rendering is centralised, so the Toaster placement and the styling props on it decide what every notification in the app looks like.

The example uses an onclick attribute in the template, which is Svelte 5 syntax rather than the older on:click, and that is a small sign of which Svelte generation this library targets.

The toast API: variants, options, promises and actions

The basic call takes a string, and the second argument is an options object that applies to that toast and to any other type:

js
toast('Event has been created');

There are named variants for the common cases, and the README lists what each one renders. toast.success, toast.info, toast.warning and toast.error each put an icon in front of the message, respectively a checkmark, a question mark, a warning icon and an error icon. The default toast has no icon, and the options object can add one, along with a description:

js
import Icon from './Icon.svelte';

toast('Event has been created', {
	description: 'Monday, January 3rd at 6:00pm',
	icon: Icon
});

Two of the variants carry behaviour rather than decoration. The action variant renders a button, which is the undo pattern:

js
toast('Event has been created', {
	action: {
		label: 'Undo',
		onClick: () => console.log('Undo')
	}
});

The promise variant is the one worth reading twice, because it manages state across time. It starts in a loading state and updates automatically once the promise resolves or fails, and the success and error messages can be functions of the result:

js
toast.promise(() => new Promise((resolve) => setTimeout(resolve, 2000)), {
	loading: 'Loading',
	success: 'Success',
	error: 'Error'
});

A component can be passed as the first argument instead of a string, which renders custom content while keeping the default styling and behaviour, and toast.custom is the unstyled counterpart of that.

Styling in two modes: the preset look and the unstyled map

There are two styling paths, and the README is clear that they are different in kind rather than in degree.

The first is the built in look, adjusted through props on the Toaster component. The theme prop switches between light and dark, the position prop places the stack, and the README enumerates the six available positions: top-left, top-center, top-right, bottom-left, bottom-center and bottom-right, with bottom-right as the default. The expand prop makes the toasts expanded by default, and visibleToasts changes how many are shown, three by default. richColors makes the error and success states more colourful, closeButton adds a close button that appears on hover, and offset sets the distance from the screen edge, for example 80px.

Global styling for every toast goes through toastOptions, which accepts a style string, a class, and a descriptionClass:

svelte
<Toaster
	toastOptions={{
		style: 'background: red;',
		class: 'my-toast',
		descriptionClass: 'my-toast-description'
	}}
/>

The same object can be passed when calling toast, which is how one notification is styled differently from the rest.

The second path is the unstyled mode, and the README calls it the preferred way to style the toasts with Tailwind. Setting unstyled: true hands over the appearance entirely, and a classes map names the parts of the toast so each one can be targeted:

svelte
<Toaster
	toastOptions={{
		unstyled: true,
		classes: {
			toast: 'bg-blue-400',
			title: 'text-red-400',
			description: 'text-red-400',
			actionButton: 'bg-zinc-400',
			cancelButton: 'bg-orange-400',
			closeButton: 'bg-lime-400'
		}
	}}
/>

The class map is also usable per toast type, with error, success, warning and info keys, which is how a team can give error toasts a red background without restyling the success ones.

Snippets for icons and a headless variant for full control

The default icons are replaced with Svelte 5 snippets, which is the mechanism that a Svelte 5 only library can use and a good indicator of the generation this port targets:

svelte
<Toaster>
	{#snippet loadingIcon()}
		<LoadingIcon />
	{/snippet}
	{#snippet successIcon()}
		<SuccessIcon />
	{/snippet}
	{#snippet errorIcon()}
		<ErrorIcon />
	{/snippet}
</Toaster>

The README documents snippets for loadingIcon, successIcon, errorIcon, infoIcon and warningIcon, and they are passed as children of the Toaster component, so the Toaster both renders the stack and accepts the icon set. A component passed in this way keeps the default styling around it while replacing the glyph.

For teams that want neither the default styling nor the class map, there is the headless path. toast.custom renders an unstyled toast with a custom component while keeping the functionality, and the example shows the component closing itself through a dispatched event:

svelte
<script>
	import { createEventDispatcher } from 'svelte';

	const dispatch = createEventDispatcher();
</script>

<div>
	This is a custom component <button onclick={() => dispatch('closeToast')}>close</button>
</div>
js
import HeadlessToast from './HeadlessToast.svelte';

toast.custom(HeadlessToast);

The headless mode is the closest this library comes to a building block: the timing, the dismissal and the stacking are still Sonner's, and only the markup is yours. That is also the point at which you should check whether you want the library at all, because a component library that renders the toast container, positions it, times it and stacks it has already made most of the decisions for you.

Programmatic control: ids, dismissal and reading the visible toasts

Toasts are addressable. The toast function returns an id, and passing that id back to a later call updates the existing toast in place, which is how a pending notification becomes a success notification without a second entry appearing in the stack:

js
const toastId = toast('Sonner');

toast.success('Toast has been updated', {
	id: toastId
});

Removal is likewise explicit. toast.dismiss(id) removes one toast, and calling toast.dismiss() with no argument removes all of them at once:

js
const toastId = toast('Event has been created');

toast.dismiss(toastId);

Reading the current state is done through the useSonner hook, which returns the visible toasts and, in the Svelte 5 style shown in the README, exposes them through an effect:

ts
const sonner = useSonner();

$effect(() => console.log(sonner.toasts));

That is the mechanism to reach for if a screen reader or a status region needs to mirror what the user sees, rather than maintaining a second copy of the notification state in your own store.

Timing is controlled per toast or globally. The duration property sets how long a toast stays, on the Toaster it applies to all of them, and a persistent toast is expressed with the infinite duration value:

js
toast('Event has been created', {
	duration: Number.POSITIVE_INFINITY
});

The README also documents onDismiss and onAutoClose callbacks, which is how you can find out that a notification expired on its own rather than being closed by the user.

Maintenance, testing, and where a port is the wrong tool

The project is small and the toolchain is modern. The scripts in package.json cover the whole cycle: vite for development, svelte-kit sync and svelte-check for type checking, eslint and prettier for linting and formatting, svelte-package with publint for building the distributable package, and changeset publish for releasing. Tests are split, with Playwright driving the end to end behaviour and Vitest for unit tests, and the repository carries a playwright.config.ts and a tests directory. A library whose visible behaviour is timing and stacking is exactly the kind of thing end to end tests are for, and the presence of Playwright here is a good sign.

The port carries the usual risk of a port. Behaviour here is inherited from Sonner, and the questions the README does not answer are about the boundary between the two projects: which upstream release the port currently matches, how quickly upstream changes are pulled in, and which differences are deliberate. The README describes the library as a port and links to Sonner, but it does not state a tracking policy or a compatibility matrix. If you need the exact upstream behaviour, check the upstream project and the port's own change log before assuming they are the same.

The other limit is the one the README already calls out with the word opinionated. The layout, the timing and the visual language are Sonner's, and the unstyled and headless modes relax the styling without changing the structure. A team whose design system dictates the markup of a notification, or which needs a toast to be a live region with particular semantics, will be fighting the library. In that case a headless component of your own, or the platform's own dialog primitives, is the smaller dependency.

Finally, the peer dependency on svelte ^5.0.0 rules the library out for Svelte 4 projects entirely, and the runed dependency means the effective dependency surface of your app grows by one package, which is small but not nothing for a component that only shows notifications.

Editorial conclusion

Adopt svelte-sonner if you are on Svelte 5 and want Sonner's behaviour and API, and if the default styling is close enough to what you want. Do not adopt it if you are still on Svelte 4, since the peer dependency asks for svelte ^5.0.0, or if your design system requires full control of the markup, because the unstyled mode and the headless variant still keep Sonner's structure and behaviour. Verify first by mounting a single Toaster component and calling toast() from a non component context, and by checking the version you install against the upstream Sonner release the port tracks, since a divergence there is where a port usually drifts.

Frequently asked questions

How do I install svelte-sonner and show my first toast?

Install it with npm i svelte-sonner, yarn add svelte-sonner or pnpm add svelte-sonner, then mount the Toaster component once and import the toast function. After that you can call toast('My first toast') from anywhere in the app, including outside a component.

How do I show a toast that follows the state of a promise?

Use toast.promise with the promise and an object holding loading, success and error messages. The toast starts in a loading state and updates automatically when the promise resolves or fails, and the success and error values can be functions that receive the result so the message can include it.

How do I style svelte-sonner toasts with Tailwind?

Set unstyled: true in toastOptions and pass a classes map with keys such as toast, title, description, actionButton, cancelButton and closeButton, each holding the utility classes for that part. The same options can be passed when calling toast to style one notification differently.

How do I dismiss a toast in svelte-sonner, and how do I update one?

The toast function returns an id, which you can pass back to a later call to update the existing toast in place. For removal, toast.dismiss(id) dismisses one toast and toast.dismiss() with no argument dismisses all of them at once.

Can I replace the default icons in svelte-sonner?

Yes. The Toaster component accepts Svelte 5 snippets named loadingIcon, successIcon, errorIcon, infoIcon and warningIcon, and each snippet renders the icon for that toast type. For a completely custom toast, toast.custom renders an unstyled toast with a component of your own while keeping the library's behaviour.

Official sources

  1. License: MIT
  2. Project website
  3. README
  4. Releases
  5. wobsoriano/svelte-sonner on GitHub
Add this badge to your README

If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.

Add this badge to your README

markdown
[![Hysen Labs](https://hysenlabs.com/badge/wobsoriano-svelte-sonner.svg)](https://hysenlabs.com/projects/wobsoriano-svelte-sonner)

Was this useful?