# Styling Sonner Toasts: Advanced Guide

# Introduction

Sonner is the best toast library for React. But the docs barely cover advanced styling. This is everything I learned building a custom dark toast system with colored rails, action buttons, and fast animations.

# Start with unstyled mode.

Set `unstyled: true` on toastOptions. This strips every default style. You own everything.

```tsx
<Toaster
  position="top-right"
  visibleToasts={5}
  gap={8}
  toastOptions={{
    unstyled: true,
    classNames: {
      toast:
        "flex items-center gap-3 w-[356px] rounded-xl border border-zinc-800 bg-zinc-950 px-4 py-3.5 shadow-lg",
      title: "text-[13px] font-semibold text-zinc-100",
      description: "text-[11px] text-zinc-500",
    },
  }}
/>
```

The toast element is an `li`. It becomes your flex container. Every child sits inside it as a flat sibling.

# The DOM structure matters.

Sonner renders this inside each toast.

```plaintext
li[data-sonner-toast]
  div[data-icon]        <- variant icon
  div[data-content]     <- title + description wrapper
  button[data-cancel]   <- cancel button (if provided)
  button[data-action]   <- action button (if provided)
```

No wrapper around the buttons. They are direct children of the `li`. This means `ml-auto` on the first button would work. But it is cleaner to set `flex: 1` on `[data-content]` so the text area fills remaining space and pushes buttons right.

```css
[data-sonner-toaster] [data-sonner-toast] [data-content] {
  flex: 1;
  min-width: 0;
}
```

# Per-variant styling with classNames.

The `classNames` object on toastOptions accepts `success`, `error`, and `info` keys. These classes get added to the toast `li` alongside the base `toast` class.

```tsx
classNames: {
  toast: 'flex items-center gap-3 rounded-xl border border-zinc-800 bg-zinc-950 px-4 py-3.5',
  success: 'border-l-[3px] !border-l-green-500 [&_[data-icon]]:text-green-500',
  error: 'border-l-[3px] !border-l-red-500 [&_[data-icon]]:text-red-500',
  info: 'border-l-[3px] !border-l-blue-500 [&_[data-icon]]:text-blue-500',
}
```

The `!border-l-*` needs `!important` because the base `border` class sets all sides. The `[&_[data-icon]]` selector targets the icon wrapper to color Sonner's built-in SVG icons.

# Loading toasts are different.

`toast.loading()` does not accept a variant. It renders a spinner inside `div[data-icon] > .sonner-loader`. The loader has `position: absolute` and `left: 50%` by default. This centers it in the toast instead of sitting on the left.

Fix it with CSS.

```css
[data-sonner-toaster] [data-sonner-toast][data-type="loading"] {
  border-left: 3px solid #3b82f6;
}

[data-sonner-toaster] [data-sonner-toast][data-type="loading"] [data-icon] {
  color: #3b82f6;
}

[data-sonner-toaster] [data-sonner-toast][data-type="loading"] .sonner-loader {
  position: static;
  transform: none;
}
```

# Custom loading spinner.

Sonner's default spinner is the iOS-style radial loader. You can replace it globally.

```tsx
function LoadingSpinner() {
  return (
    <svg
      className="animate-spin"
      fill="none"
      height="18"
      viewBox="0 0 18 18"
      width="18"
    >
      <circle cx="9" cy="9" r="7" stroke="#262626" strokeWidth="2" />
      <path
        d="M9 2a7 7 0 0 1 7 7"
        stroke="#3B82F6"
        strokeLinecap="round"
        strokeWidth="2"
      />
    </svg>
  );
}

<Toaster icons={{ loading: <LoadingSpinner /> }} />;
```

A dark track circle with a blue arc that rotates. Looks clean on dark backgrounds.

# Action buttons.

Sonner supports `action` and `cancel` on individual toasts.

```ts
toast.error("Upload failed", {
  description: "Could not reach the server.",
  cancel: { label: "Dismiss", onClick: () => {} },
  action: { label: "Retry", onClick: () => handleRetry() },
});
```

Style them through classNames on the Toaster.

```tsx
classNames: {
  actionButton: 'text-[11px] font-semibold px-3 py-1.5 rounded-md bg-red-500/15 text-red-400',
  cancelButton: 'text-[11px] font-medium text-zinc-500 hover:text-zinc-300 transition-colors',
}
```

The cancel button renders before the action button in the DOM. Both are flat children of the toast `li`. No wrapper div.

# Faster animations.

Sonner's default entrance animation is 400ms. Override it with a CSS variable.

```css
[data-sonner-toaster] [data-sonner-toast] {
  --toast-animation-duration: 200ms;
}
```

The stack expand and collapse also uses 400ms transitions. Override those too.

```css
[data-sonner-toaster] [data-sonner-toast] {
  --toast-animation-duration: 200ms;
  transition:
    transform 200ms ease,
    opacity 200ms ease,
    height 200ms ease,
    box-shadow 200ms ease !important;
}
```

The `!important` is needed to override Sonner's inline transition styles.

# Data attributes for targeting.

Every toast exposes data attributes you can use for styling.

```plaintext
data-type="success|error|info|loading"
data-expanded="true|false"
data-front="true|false"
data-visible="true|false"
data-mounted="true|false"
data-x-position="left|center|right"
data-y-position="top|bottom"
```

Example. Style only the front toast differently.

```css
[data-sonner-toast][data-front="true"] {
  box-shadow: 0 8px 32px rgba(0, 0, 0, 0.4);
}
```

# The full setup.

Here is a complete dark theme Toaster with colored left rails, custom spinner, styled action buttons, and fast animations.

```tsx
<Toaster
  position="top-right"
  visibleToasts={5}
  gap={8}
  icons={{ loading: <LoadingSpinner /> }}
  toastOptions={{
    unstyled: true,
    classNames: {
      toast:
        "flex items-center gap-3 w-[356px] rounded-xl border border-[#262626] bg-[#1A1A1A] px-4 py-3.5 shadow-lg",
      title: "text-[13px] font-semibold text-[#F0F0F0]",
      description: "text-[11px] text-[#636363]",
      actionButton:
        "text-[11px] font-semibold px-3 py-1.5 rounded-md bg-red-500/15 text-red-400 hover:bg-red-500/25 transition-colors",
      cancelButton:
        "text-[11px] font-medium text-[#636363] hover:text-[#999] transition-colors",
      closeButton: "text-[#636363] hover:text-[#999] transition-colors",
      success:
        "border-l-[3px] !border-l-green-500 [&_[data-icon]]:text-green-500",
      info: "border-l-[3px] !border-l-blue-500 [&_[data-icon]]:text-blue-500",
      error: "border-l-[3px] !border-l-red-500 [&_[data-icon]]:text-red-500",
    },
  }}
/>
```

```css
[data-sonner-toaster] [data-sonner-toast] [data-content] {
  flex: 1;
  min-width: 0;
}

[data-sonner-toaster] [data-sonner-toast][data-type="loading"] {
  border-left: 3px solid #3b82f6;
}

[data-sonner-toaster] [data-sonner-toast][data-type="loading"] [data-icon] {
  color: #3b82f6;
}

[data-sonner-toaster] [data-sonner-toast][data-type="loading"] .sonner-loader {
  position: static;
  transform: none;
}

[data-sonner-toaster] [data-sonner-toast] {
  --toast-animation-duration: 200ms;
  transition:
    transform 200ms ease,
    opacity 200ms ease,
    height 200ms ease,
    box-shadow 200ms ease !important;
}
```

# Gotchas.

The loading spinner has `position: absolute` by default. You must override it to `static` or it floats to the center of the toast.

The `unstyled` mode strips everything. Including flex layout on the content wrapper. You need the `[data-content] { flex: 1 }` CSS rule or buttons will not push to the right.

Action button colors are global through classNames. If you need different colors per variant you have to use the variant class selector. For example `[&_[data-action]]:bg-green-500/15` inside the `success` className.

The `!important` on `border-l-*` is required. The base `border` shorthand in the toast class sets `border-left` which overrides your variant border without it.

`visibleToasts` controls how many toasts exist in the DOM. Not just how many show in the collapsed stack. Set it higher than 3 if you want users to see all toasts when they hover to expand.
