Toast notifications, parity target Sonner.
Unlike every other SaladUI component, toaster/1 does not manage a single
open/closed state machine — it mounts once per page and manages an unbounded,
dynamically created stack of independent toast cards entirely in JavaScript
(assets/salad_ui/components/toast.js).
Mounting
<.toaster id="toaster" position="bottom-right" />Triggering toasts
Use the helpers below from any handle_event/3:
def handle_event("save", _params, socket) do
{:noreply, SaladUI.Toast.toast_success(socket, "Saved!")}
endRich-content toasts
Declare a :template next to the toaster and reference it by name:
<.toaster id="toaster">
<:template name="new-follower">
<img src={@avatar_url} class="size-8 rounded-full" />
<p><%= @name %> followed you</p>
</:template>
</.toaster>
SaladUI.Toast.toast(socket, template: "new-follower")Customization
SaladUI ships no predefined per-variant palette — every toast renders in the
card's neutral default (border-border bg-background text-foreground)
unless you opt in, either toaster-wide or per call.
Toaster-wide, applied to every card of that variant:
<.toaster
id="toaster"
colors={%{success: "border-emerald-500/30 bg-emerald-50 text-emerald-900"}}
/>Per-toast, overriding the toaster-wide default for just this call:
SaladUI.Toast.toast_success(socket, "Saved!",
color: "border-emerald-500/30 bg-emerald-50 text-emerald-900"
)color/colors replace the card's default border/background/text as one
unit — the icon and title inherit the resulting text color, while the
description keeps its own muted color. The toaster's class attr and a
per-toast class opt instead append to the default classes, so reach for
!-prefixed utilities there to reliably win the cascade:
SaladUI.Toast.toast(socket, "Something broke",
class: "!border-fuchsia-500/30 !bg-fuchsia-50 !text-fuchsia-900"
)unstyled: true drops every built-in class, leaving bare data-part
elements for class to style from scratch:
SaladUI.Toast.toast(socket, "Bare toast", unstyled: true, class: "my-toast")action/cancel accept an optional value map, merged into the event
payload pushed back to the LiveView (alongside id):
SaladUI.Toast.toast(socket, "File deleted",
action: %{label: "Undo", event: "undo_delete", value: %{file_id: file.id}},
cancel: %{label: "Dismiss"}
)
def handle_event("undo_delete", %{"id" => _id, "file_id" => file_id}, socket) do
# restore file_id
{:noreply, socket}
endPhoenix flash integration
Pass flash to render flash messages as toasts instead of a second
notification system:
<.toaster id="toaster" flash={@flash} />
Summary
Functions
Drop-in-shaped alongside Phoenix.LiveView.put_flash/3 /
Phoenix.Controller.put_flash/3 — same arity, same call sites, works on both.
Push a toast to a mounted <.toaster> (see toaster/1).
Removes one toast, or every toast on the given toaster if id is omitted.
Same as toast/3, with variant: "error".
Bridges @flash into toasts on the given toaster without patching the
toaster's own DOM subtree (which would tear down its live stack/timers).
Same as toast/3, with variant: "info".
Same as toast/3, with variant: "success".
Transitions an existing toast (by id) to a new variant/content, patched in place.
Same as toast/3, with variant: "warning".
Mounts the toast stack. Render exactly once per page (typically the root layout).
Functions
Drop-in-shaped alongside Phoenix.LiveView.put_flash/3 /
Phoenix.Controller.put_flash/3 — same arity, same call sites, works on both.
On a %Socket{} this behaves like toast/3 with variant: kind. On a
%Plug.Conn{} (no toaster hook exists to push a command to — the page
hasn't rendered yet) it falls back to Phoenix.Controller.put_flash/3, so a
controller action followed by a redirect still shows something instead of
silently dropping the message; once the LiveView the redirect lands on
renders <.toaster flash={@flash} />, the flash bridge (toast_flash/1)
picks it up and converts it to a real toast on connect.
Push a toast to a mounted <.toaster> (see toaster/1).
message is either a string, used as the toast's title, or (for
template-based toasts, see toaster/1's :template slot) a keyword
list/map of opts with no title:
socket = SaladUI.Toast.toast(socket, "Saved!")
socket = SaladUI.Toast.toast(socket, "Saved!", description: "Changes are live")
socket = SaladUI.Toast.toast(socket, template: "new-follower", variant: "default")Call toast/4 directly to target a <.toaster> other than the default "toaster" id.
See toaster/1's moduledoc for the full opts table.
Removes one toast, or every toast on the given toaster if id is omitted.
Same as toast/3, with variant: "error".
Bridges @flash into toasts on the given toaster without patching the
toaster's own DOM subtree (which would tear down its live stack/timers).
Usually rendered implicitly via toaster/1's :flash attribute; call it
directly when the toaster and the LiveView holding @flash are mounted
separately.
Attributes
id(:string) - Defaults to"toast-flash-bridge".flash(:map) (required)toaster(:string) - Defaults to"toaster".
Same as toast/3, with variant: "info".
Same as toast/3, with variant: "success".
Transitions an existing toast (by id) to a new variant/content, patched in place.
Same as toast/3, with variant: "warning".
Mounts the toast stack. Render exactly once per page (typically the root layout).
Server commands
"add"- Creates and mounts a new toast card, arms its timer."update"- Patches an existing card in place, keyed byid."dismiss"- Plays the exit animation and removes a card (or every card ifidis omitted).
Component events
:on-dismiss- Fired whenever a toast is removed, with%{id:, reason:}in the payload.
Attributes
id(:string) - Defaults to"toaster".position(:string) - Defaults to"bottom-right". Must be one of"top-left","top-center","top-right","bottom-left","bottom-center", or"bottom-right".expand(:boolean) - Stack fully expanded instead of collapsed. Defaults tofalse.visible-toasts(:integer) - Max stacked cards before overflow is collapsed. Defaults to3.gap(:integer) - px gap between stacked toasts when expanded. Defaults to14.swipe-direction(:string) - Defaults tonil.Must be one of"up","down","left","right", ornil.width(:integer) - Fixed card width in px. Defaults to356.offset(:any) - px gap from the viewport edge, or a %{top:, right:, bottom:, left:} map. Defaults to24.mobile-offset(:any) - Same as :offset, applied below the 600px breakpoint. Defaults to16.hotkey(:string) - Keyboard shortcut that moves focus into the front toast. Defaults to"Alt+T".icons(:map) - toaster-wide icon overrides, e.g. %{success: "hero-sparkles"}. Defaults to%{}.colors(:map) - per-variant toast color override, e.g. %{success: "border-emerald-500/30 bg-emerald-50 text-emerald-900"} — replaces the card's default border/background/text (icon + title inherit the text color; description keeps its own muted color) for that variant. No variant ships with a predefined color. Defaults to%{}.class(:string) - extra classes merged onto every card — use!-prefixed utilities, e.g. "!bg-secondary", to reliably override a default color class. Defaults tonil.flash(:map) - when given, also renders the flash-to-toast bridge (see toast_flash/1). Defaults tonil.on-dismiss(:any) - Handler fired when any toast is removed. Payload includesidandreason. Defaults tonil.
Slots
template- named rich-content template, referenced fromtoast(socket, template: name). Accepts attributes:name(:string) (required)