Sonner
Toast notifications via Sonner. Types: default, success, info, warning, error, loading, plus a promise toast.
import { Button } from "@/components/ui/button";import { Toaster, toast } from "@/components/ui/sonner";
export default function Demo() { return ( <div className="flex flex-wrap gap-2"> <Toaster /> <Button variant="outline" onClick={() => toast("Copied to clipboard")}> Default </Button> <Button variant="outline" onClick={() => toast.success("Joined MAG4", { description: "98,209 shares - tx 4f9a...c21", }) } > Success </Button> <Button variant="outline" onClick={() => toast.info("Rebalance scheduled", { description: "Next keeper run at 09:00 UTC", }) } > Info </Button> <Button variant="outline" onClick={() => toast.warning("Drift is 6.2%", { description: "Above your 5% limit.", }) } > Warning </Button> <Button variant="outline" onClick={() => toast.error("Signature rejected", { description: "The wallet closed the request.", }) } > Error </Button> <Button variant="outline" onClick={() => toast.promise(new Promise((r) => setTimeout(r, 2000)), { loading: "Deploying vault...", success: "Vault deployed", error: "Deploy failed", }) } > Promise </Button> </div> );}<script setup lang="ts">import { Button } from "@/components/ui/button";import { Toaster, toast } from "@/components/ui/sonner";</script>
<template> <div class="flex flex-wrap gap-2"> <Toaster /> <Button variant="outline" @click="toast('Copied to clipboard')">Default</Button> <Button variant="outline" @click="toast.success('Joined MAG4', { description: '98,209 shares - tx 4f9a...c21' })" > Success </Button> <Button variant="outline" @click="toast.info('Rebalance scheduled', { description: 'Next keeper run at 09:00 UTC' })" > Info </Button> <Button variant="outline" @click="toast.warning('Drift is 6.2%', { description: 'Above your 5% limit.' })" > Warning </Button> <Button variant="outline" @click="toast.error('Signature rejected', { description: 'The wallet closed the request.' })" > Error </Button> <Button variant="outline" @click=" toast.promise(new Promise((r) => setTimeout(r, 2000)), { loading: 'Deploying vault...', success: () => 'Vault deployed', error: () => 'Deploy failed', }) " > Promise </Button> </div></template><script lang="ts"> import { Button } from "$lib/components/ui/button"; import { Toaster, toast } from "$lib/components/ui/sonner";</script>
<div class="flex flex-wrap gap-2"> <Toaster /> <Button variant="outline" onclick={() => toast("Copied to clipboard")}>Default</Button> <Button variant="outline" onclick={() => toast.success("Joined MAG4", { description: "98,209 shares - tx 4f9a...c21" })} > Success </Button> <Button variant="outline" onclick={() => toast.info("Rebalance scheduled", { description: "Next keeper run at 09:00 UTC" })} > Info </Button> <Button variant="outline" onclick={() => toast.warning("Drift is 6.2%", { description: "Above your 5% limit." })} > Warning </Button> <Button variant="outline" onclick={() => toast.error("Signature rejected", { description: "The wallet closed the request." })} > Error </Button> <Button variant="outline" onclick={() => toast.promise(new Promise((r) => setTimeout(r, 2000)), { loading: "Deploying vault...", success: "Vault deployed", error: "Deploy failed", })} > Promise </Button></div>Installation
Section titled “Installation”npx shadcn@latest add @edmi-ui/sonnerpnpm dlx shadcn@latest add @edmi-ui/sonneryarn dlx shadcn@latest add @edmi-ui/sonnerbunx shadcn@latest add @edmi-ui/sonnerPick your package manager above; the choice is remembered. Add the @edmi-ui registry first, see Getting started.
Install the dependencies:
Terminal window npm install sonner next-themesTerminal window pnpm add sonner next-themesTerminal window yarn add sonner next-themesTerminal window bun add sonner next-themesAdd the Edmi components this one builds on:
elevation.Copy the code from Component source into your project and update the import paths to match your aliases.
npx shadcn-vue@latest add @edmi-ui/sonnerpnpm dlx shadcn-vue@latest add @edmi-ui/sonneryarn dlx shadcn-vue@latest add @edmi-ui/sonnerbunx shadcn-vue@latest add @edmi-ui/sonnerPick your package manager above; the choice is remembered. Add the @edmi-ui registry first, see Getting started.
Install the dependencies:
Terminal window npm install vue-sonnerTerminal window pnpm add vue-sonnerTerminal window yarn add vue-sonnerTerminal window bun add vue-sonnerAdd the Edmi components this one builds on:
elevation.Copy the code from Component source into your project and update the import paths to match your aliases.
npx shadcn-svelte@latest add https://viandwi24.github.io/edmi-ui/r/svelte/sonner.jsonpnpm dlx shadcn-svelte@latest add https://viandwi24.github.io/edmi-ui/r/svelte/sonner.jsonyarn dlx shadcn-svelte@latest add https://viandwi24.github.io/edmi-ui/r/svelte/sonner.jsonbunx --bun shadcn-svelte@latest add https://viandwi24.github.io/edmi-ui/r/svelte/sonner.jsonPick your package manager above; the choice is remembered. Add the @edmi-ui registry first, see Getting started.
Add the Edmi components this one builds on:
elevation.Copy the code from Component source into your project and update the import paths to match your aliases.
import { Button } from "@/components/ui/button";import { Toaster, toast } from "@/components/ui/sonner";
<div className="flex flex-wrap gap-2"> <Toaster /> <Button variant="outline" onClick={() => toast("Copied to clipboard")}> Default </Button> <Button variant="outline" onClick={() => toast.success("Joined MAG4", { description: "98,209 shares - tx 4f9a...c21", }) } > Success </Button> <Button variant="outline" onClick={() => toast.info("Rebalance scheduled", { description: "Next keeper run at 09:00 UTC", }) } > Info </Button> {/* … */}</div><script setup lang="ts">import { Button } from "@/components/ui/button";import { Toaster, toast } from "@/components/ui/sonner";</script>
<template> <div class="flex flex-wrap gap-2"> <Toaster /> <Button variant="outline" @click="toast('Copied to clipboard')">Default</Button> <Button variant="outline" @click="toast.success('Joined MAG4', { description: '98,209 shares - tx 4f9a...c21' })" > Success </Button> <Button variant="outline" @click="toast.info('Rebalance scheduled', { description: 'Next keeper run at 09:00 UTC' })" > Info </Button> <Button variant="outline" @click="toast.warning('Drift is 6.2%', { description: 'Above your 5% limit.' })" > Warning </Button> <!-- … --> </div></template><script lang="ts"> import { Button } from "$lib/components/ui/button"; import { Toaster, toast } from "$lib/components/ui/sonner";</script>
<div class="flex flex-wrap gap-2"> <Toaster /> <Button variant="outline" onclick={() => toast("Copied to clipboard")}>Default</Button> <Button variant="outline" onclick={() => toast.success("Joined MAG4", { description: "98,209 shares - tx 4f9a...c21" })} > Success </Button> <Button variant="outline" onclick={() => toast.info("Rebalance scheduled", { description: "Next keeper run at 09:00 UTC" })} > Info </Button> <Button variant="outline" onclick={() => toast.warning("Drift is 6.2%", { description: "Above your 5% limit." })} > Warning </Button> <!-- … --></div>Component source
Section titled “Component source”"use client";
import { useTheme } from "next-themes";import { Toaster as Sonner, type ToasterProps, toast } from "sonner";import { CheckCircleIcon, InfoIcon, WarningIcon, XCircleIcon, SpinnerIcon } from "@phosphor-icons/react";
import { type Elevation, useElevation } from "@/registry/edmi/ui/elevation";
// ✦ depth (v4): floating is the natural level of a toast (overlay role in layered mode).const toastElevation = { sunken: "shadow-none!", flat: "shadow-none!", raised: "border-transparent! shadow-raised!", floating: "border-transparent! shadow-floating!",};
// Soft fill + tinted 40% border per type (DESIGN §4.12); default stays a solid popover chip.// Sonner's own selectors are more specific than utilities, hence the important modifier.const Toaster = ({ elevation, ...props}: ToasterProps & { /** ✦ depth for every toast: flat 0, raised +1 (bevel), floating +2 (bevel + drop). */ elevation?: Elevation;}) => { const level = useElevation(elevation, "overlay"); // next-themes is optional in a Vite app: without a ThemeProvider useTheme() returns "system". const { theme = "system" } = useTheme();
return ( <Sonner theme={theme as ToasterProps["theme"]} className="toaster group" icons={{ success: ( <CheckCircleIcon className="size-4 text-success-text" /> ), info: ( <InfoIcon className="size-4 text-info-text" /> ), warning: ( <WarningIcon className="size-4 text-warning-text" /> ), error: ( <XCircleIcon className="size-4 text-destructive-text" /> ), loading: ( <SpinnerIcon className="size-4 animate-spin" /> ), }} style={ { "--normal-bg": "var(--popover)", "--normal-text": "var(--popover-foreground)", "--normal-border": "var(--border)", "--border-radius": "var(--radius-xl)", "--width": "360px", } as React.CSSProperties } toastOptions={{ classNames: { toast: `cn-toast ${toastElevation[level]} text-[13.5px] font-sans`, title: "font-medium", description: "text-muted-foreground!", success: "bg-success-soft! border-[color-mix(in_srgb,var(--success)_40%,var(--popover))]!", info: "bg-info-soft! border-[color-mix(in_srgb,var(--info)_40%,var(--popover))]!", warning: "bg-warning-soft! border-[color-mix(in_srgb,var(--warning)_40%,var(--popover))]!", error: "bg-destructive-soft! border-[color-mix(in_srgb,var(--destructive)_40%,var(--popover))]!", }, }} {...props} /> );};
// ✦ Re-exported so apps import `toast` and `Toaster` from one place.export { Toaster, toast };<script setup lang="ts">import type { ToasterProps } from 'vue-sonner'import { CircleCheckIcon, InfoIcon, Loader2Icon, OctagonXIcon, TriangleAlertIcon, XIcon } from '@lucide/vue'import { reactiveOmit } from '@vueuse/core'import { computed } from 'vue'import { Toaster as Sonner } from 'vue-sonner'import 'vue-sonner/style.css'import { cn } from '@/registry/edmi/lib/utils'import { type Elevation, useElevation } from '@/registry/edmi/ui/elevation'
const props = withDefaults(defineProps<ToasterProps & { elevation?: Elevation }>(), { elevation: undefined })
// ✦ depth (v4): floating is the natural level of a toast (overlay role in layered mode).const toastElevation = { sunken: 'shadow-none!', flat: 'shadow-none!', raised: 'border-transparent! shadow-raised!', floating: 'border-transparent! shadow-floating!',}const level = useElevation(() => props.elevation, 'overlay')const delegatedProps = reactiveOmit(props, 'elevation')
// Soft fill + tinted 40% border per type (DESIGN 4.12); default stays a solid popover chip.// vue-sonner's own selectors are more specific than utilities, hence the important modifier.const toastClasses = computed(() => ({ toast: cn('cn-toast text-[13.5px] font-sans', toastElevation[level.value]), title: 'font-medium', description: 'text-muted-foreground!', success: 'bg-success-soft! border-[color-mix(in_srgb,var(--success)_40%,var(--popover))]!', info: 'bg-info-soft! border-[color-mix(in_srgb,var(--info)_40%,var(--popover))]!', warning: 'bg-warning-soft! border-[color-mix(in_srgb,var(--warning)_40%,var(--popover))]!', error: 'bg-destructive-soft! border-[color-mix(in_srgb,var(--destructive)_40%,var(--popover))]!',}))</script>
<template> <Sonner v-bind="delegatedProps" :class="cn('toaster group', props.class)" :toast-options="{ ...props.toastOptions, classes: { ...toastClasses, ...props.toastOptions?.classes } }" :style="{ '--normal-bg': 'var(--popover)', '--normal-text': 'var(--popover-foreground)', '--normal-border': 'var(--border)', '--border-radius': 'var(--radius-xl)', '--width': '360px', }" > <template #success-icon> <CircleCheckIcon class="size-4 text-success-text" /> </template> <template #info-icon> <InfoIcon class="size-4 text-info-text" /> </template> <template #warning-icon> <TriangleAlertIcon class="size-4 text-warning-text" /> </template> <template #error-icon> <OctagonXIcon class="size-4 text-destructive-text" /> </template> <template #loading-icon> <div> <Loader2Icon class="size-4 animate-spin" /> </div> </template> <template #close-icon> <XIcon class="size-4" /> </template> </Sonner></template>// ✦ Re-exported so apps import `toast` and `Toaster` from one place.
export { toast } from "vue-sonner";export { default as Toaster } from "./Sonner.vue";<script lang="ts"> import { mode } from "mode-watcher"; import { Toaster as Sonner, type ToasterProps as SonnerProps } from "svelte-sonner"; import { cn } from "$lib/utils.js"; import { type Elevation, useElevation } from "$lib/registry/ui/elevation/index.js"; import SpinnerIcon from "phosphor-svelte/lib/Spinner"; import CheckCircleIcon from "phosphor-svelte/lib/CheckCircle"; import XCircleIcon from "phosphor-svelte/lib/XCircle"; import InfoIcon from "phosphor-svelte/lib/Info"; import WarningIcon from "phosphor-svelte/lib/Warning";
let { elevation = "auto", ...restProps }: SonnerProps & { /** ✦ depth for every toast: flat 0, raised +1 (bevel), floating +2 (bevel + drop). */ elevation?: Elevation; } = $props();
// ✦ depth (v4): floating is the natural level of a toast (overlay role in layered mode). const toastElevation = { sunken: "shadow-none!", flat: "shadow-none!", raised: "border-transparent! shadow-raised!", floating: "border-transparent! shadow-floating!", }; const level = useElevation(() => elevation, "overlay");
// Soft fill + tinted 40% border per type (DESIGN 4.12); default stays a solid popover chip. // svelte-sonner's own selectors are more specific than utilities, hence the important modifier.</script>
<Sonner theme={mode.current} class="toaster group" style="--normal-bg: var(--popover); --normal-text: var(--popover-foreground); --normal-border: var(--border); --border-radius: var(--radius-xl); --width: 360px;" toastOptions={{ classes: { toast: cn("cn-toast text-[13.5px] font-sans", toastElevation[level.current]), title: "font-medium", description: "text-muted-foreground!", success: "bg-success-soft! border-[color-mix(in_srgb,var(--success)_40%,var(--popover))]!", info: "bg-info-soft! border-[color-mix(in_srgb,var(--info)_40%,var(--popover))]!", warning: "bg-warning-soft! border-[color-mix(in_srgb,var(--warning)_40%,var(--popover))]!", error: "bg-destructive-soft! border-[color-mix(in_srgb,var(--destructive)_40%,var(--popover))]!", }, }} {...restProps}> {#snippet loadingIcon()} <SpinnerIcon class="size-4 animate-spin" /> {/snippet} {#snippet successIcon()} <CheckCircleIcon class="size-4 text-success-text" /> {/snippet} {#snippet errorIcon()} <XCircleIcon class="size-4 text-destructive-text" /> {/snippet} {#snippet infoIcon()} <InfoIcon class="size-4 text-info-text" /> {/snippet} {#snippet warningIcon()} <WarningIcon class="size-4 text-warning-text" /> {/snippet}</Sonner>// ✦ Re-exported so apps import `toast` and `Toaster` from one place.
export { toast } from "svelte-sonner";export { default as Toaster } from "./sonner.svelte";Elevation ✦
Section titled “Elevation ✦”See the Elevation guide for the levels, roles and layered mode.
Flat by default. On Toaster, raised (+1) adds the bevel and floating (+2) the bevel plus one soft drop to every toast (floating is the natural level of a toast).
import { Button } from "@/components/ui/button";import { Toaster, toast } from "@/components/ui/sonner";
export default function Demo() { return ( <div className="flex flex-wrap gap-2"> <Toaster elevation="floating" /> <Button variant="outline" onClick={() => toast("Copied to clipboard")}> Default </Button> <Button variant="outline" onClick={() => toast.success("Joined MAG4", { description: "98,209 shares - tx 4f9a...c21", }) } > Success </Button> <Button variant="outline" onClick={() => toast.info("Rebalance scheduled", { description: "Next keeper run at 09:00 UTC", }) } > Info </Button> <Button variant="outline" onClick={() => toast.warning("Drift is 6.2%", { description: "Above your 5% limit.", }) } > Warning </Button> <Button variant="outline" onClick={() => toast.error("Signature rejected", { description: "The wallet closed the request.", }) } > Error </Button> <Button variant="outline" onClick={() => toast.promise(new Promise((r) => setTimeout(r, 2000)), { loading: "Deploying vault...", success: "Vault deployed", error: "Deploy failed", }) } > Promise </Button> </div> );}<script setup lang="ts">import { Button } from "@/components/ui/button";import { Toaster, toast } from "@/components/ui/sonner";</script>
<template> <div class="flex flex-wrap gap-2"> <Toaster elevation="floating" /> <Button variant="outline" @click="toast('Copied to clipboard')">Default</Button> <Button variant="outline" @click="toast.success('Joined MAG4', { description: '98,209 shares - tx 4f9a...c21' })" > Success </Button> <Button variant="outline" @click="toast.info('Rebalance scheduled', { description: 'Next keeper run at 09:00 UTC' })" > Info </Button> <Button variant="outline" @click="toast.warning('Drift is 6.2%', { description: 'Above your 5% limit.' })" > Warning </Button> <Button variant="outline" @click="toast.error('Signature rejected', { description: 'The wallet closed the request.' })" > Error </Button> <Button variant="outline" @click=" toast.promise(new Promise((r) => setTimeout(r, 2000)), { loading: 'Deploying vault...', success: () => 'Vault deployed', error: () => 'Deploy failed', }) " > Promise </Button> </div></template><script lang="ts"> import { Button } from "$lib/components/ui/button"; import { Toaster, toast } from "$lib/components/ui/sonner";</script>
<div class="flex flex-wrap gap-2"> <Toaster elevation="floating" /> <Button variant="outline" onclick={() => toast("Copied to clipboard")}>Default</Button> <Button variant="outline" onclick={() => toast.success("Joined MAG4", { description: "98,209 shares - tx 4f9a...c21" })} > Success </Button> <Button variant="outline" onclick={() => toast.info("Rebalance scheduled", { description: "Next keeper run at 09:00 UTC" })} > Info </Button> <Button variant="outline" onclick={() => toast.warning("Drift is 6.2%", { description: "Above your 5% limit." })} > Warning </Button> <Button variant="outline" onclick={() => toast.error("Signature rejected", { description: "The wallet closed the request." })} > Error </Button> <Button variant="outline" onclick={() => toast.promise(new Promise((r) => setTimeout(r, 2000)), { loading: "Deploying vault...", success: "Vault deployed", error: "Deploy failed", })} > Promise </Button></div>| Prop | Type | Default |
|---|---|---|
Toaster |
Sonner ToasterProps |
bottom-right |
toast(title, opts) |
toast.success / info / warning / error / loading / promise |
none |
elevation |
"auto" | "sunken" | "flat" | "raised" | "floating" |
"auto" |
Mount <Toaster /> once in the app root, then call toast() anywhere. next-themes is read for the theme and falls back to system when no ThemeProvider exists (Vite apps).
✦ Edmi additions
Section titled “✦ Edmi additions”- Typed toasts use the soft fill and tinted border; the default toast is a solid popover chip.
toastis re-exported from the same module asToaster.