Sonner is an opinionated toast component for React. It owns the notification stack, animations and accessibility; you trigger toasts imperatively with toast() from anywhere in your tree. ZUI has no toast component of its own, so Sonner slots in neatly — render it from a ZUI Button and theme it with ZUI's tokens.

React only. Sonner ships a single React component. In Astro, Solid, Svelte or Vue projects, mount it inside a React island (as the live demo below does).

Trigger a few toasts — they follow this site's light/dark toggle:

Install

npm install sonner

Mount the Toaster once

<Toaster /> renders the stack and portals it to the document. Mount it a single time near the root of your app — every toast() call anywhere feeds the same instance.

import { Toaster } from 'sonner'

export function App() {
  return (
    <>
      <YourRoutes />
      <Toaster closeButton />
    </>
  )
}

In an Astro project, render the Toaster as a client island in your layout so it persists across the page:

---
import { Toaster } from 'sonner'
---

<Toaster client:load closeButton />

Trigger toasts

toast is a function with variants for the common cases. Wire them to ZUI buttons (or anything else):

import { Button } from '@mrmartineau/zui/react'
import { toast } from 'sonner'

// Default
<Button onClick={() => toast('Settings saved')}>Save</Button>

// Success / error, with an optional description
toast.success('Profile updated', { description: 'Your changes are live.' })
toast.error('Could not save', { description: 'Check your connection.' })

// With an action button
toast('New message from Alice', {
  action: { label: 'Reply', onClick: () => openReply() },
})

// Bind to a promise — shows loading, then resolves to success/error
toast.promise(saveSettings(), {
  loading: 'Saving…',
  success: 'Saved!',
  error: 'Save failed',
})

Sync the theme with ZUI

ZUI exposes the current colour scheme through the useColorScheme hook. Pass its scheme straight to the Toaster's theme prop so toasts flip with the rest of the site — including the system setting.

import { useColorScheme } from '@mrmartineau/zui/react'
import { Toaster } from 'sonner'

function AppToaster() {
  const { scheme } = useColorScheme() // 'light' | 'dark' | 'system'
  return <Toaster theme={scheme} closeButton />
}

Use ZUI colours

Sonner ships a richColors prop for coloured success/error/warning/info toasts, but its palette is fixed. To tint toasts with ZUI's colours instead, leave richColors off and style the toasts yourself with CSS.

Each toast reads --normal-bg / -text / -border (Sonner only inherits these from the toaster, so setting them directly on the toast wins) and carries a data-type attribute. Map the neutral toast onto ZUI's surface, then tint each variant from ZUI's colour scalelight-dark() covers both schemes in one declaration. This is exactly what the demo above does:

[data-sonner-toaster] {
  --width: 22rem;
  --border-radius: var(--radius-lg);
  font-family: var(--font-body);
}

/* Neutral toast → ZUI surface */
[data-sonner-toast] {
  --normal-bg: var(--color-surface);
  --normal-text: var(--color-text);
  --normal-border: var(--color-border);
}

/* Per-type tints from ZUI's colour ramps */
[data-sonner-toast][data-type='success'] {
  --normal-bg: light-dark(var(--color-green-50), var(--color-green-950));
  --normal-text: light-dark(var(--color-green-700), var(--color-green-200));
  --normal-border: light-dark(var(--color-green-200), var(--color-green-800));
}

[data-sonner-toast][data-type='error'] {
  --normal-bg: light-dark(var(--color-red-50), var(--color-red-950));
  --normal-text: light-dark(var(--color-red-700), var(--color-red-200));
  --normal-border: light-dark(var(--color-red-200), var(--color-red-800));
}

[data-sonner-toast][data-type='warning'] {
  --normal-bg: light-dark(var(--color-amber-50), var(--color-amber-950));
  --normal-text: light-dark(var(--color-amber-800), var(--color-amber-200));
  --normal-border: light-dark(var(--color-amber-200), var(--color-amber-800));
}

[data-sonner-toast][data-type='info'] {
  --normal-bg: light-dark(var(--color-blue-50), var(--color-blue-950));
  --normal-text: light-dark(var(--color-blue-700), var(--color-blue-200));
  --normal-border: light-dark(var(--color-blue-200), var(--color-blue-800));
}

Swap the hue tokens for any of ZUI's colour ramps to re-tint a variant. For finer control, hand specific elements a ZUI class with toastOptions:

<Toaster
  toastOptions={{
    classNames: { toast: 'zui-card', description: 'zui-field-description' },
  }}
/>

Notes

Theme

Copy this CSS to your project: