Skip to content
edmi-ui

Sonner

Toast notifications via Sonner. Types: default, success, info, warning, error, loading, plus a promise toast.

StackBlitz ↗StackBlitz ↗StackBlitz ↗
Terminal window
npx shadcn@latest add @edmi-ui/sonner
Terminal window
pnpm dlx shadcn@latest add @edmi-ui/sonner
Terminal window
yarn dlx shadcn@latest add @edmi-ui/sonner
Terminal window
bunx shadcn@latest add @edmi-ui/sonner

Pick your package manager above; the choice is remembered. Add the @edmi-ui registry first, see Getting started.

Terminal window
npx shadcn-vue@latest add @edmi-ui/sonner
Terminal window
pnpm dlx shadcn-vue@latest add @edmi-ui/sonner
Terminal window
yarn dlx shadcn-vue@latest add @edmi-ui/sonner
Terminal window
bunx shadcn-vue@latest add @edmi-ui/sonner

Pick your package manager above; the choice is remembered. Add the @edmi-ui registry first, see Getting started.

Terminal window
npx shadcn-svelte@latest add https://viandwi24.github.io/edmi-ui/r/svelte/sonner.json
Terminal window
pnpm dlx shadcn-svelte@latest add https://viandwi24.github.io/edmi-ui/r/svelte/sonner.json
Terminal window
yarn dlx shadcn-svelte@latest add https://viandwi24.github.io/edmi-ui/r/svelte/sonner.json
Terminal window
bunx --bun shadcn-svelte@latest add https://viandwi24.github.io/edmi-ui/r/svelte/sonner.json

Pick your package manager above; the choice is remembered. Add the @edmi-ui registry first, see Getting started.

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>
registry/ui/sonner.tsx
"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 };
registry/ui/sonner/Sonner.vue
<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>
registry/ui/sonner/index.ts
// ✦ Re-exported so apps import `toast` and `Toaster` from one place.
export { toast } from "vue-sonner";
export { default as Toaster } from "./Sonner.vue";
src/lib/registry/ui/sonner/sonner.svelte
<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>
src/lib/registry/ui/sonner/index.ts
// ✦ Re-exported so apps import `toast` and `Toaster` from one place.
export { toast } from "svelte-sonner";
export { default as Toaster } from "./sonner.svelte";

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).

StackBlitz ↗StackBlitz ↗StackBlitz ↗
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).

  • Typed toasts use the soft fill and tinted border; the default toast is a solid popover chip.
  • toast is re-exported from the same module as Toaster.