Server-rendered toast notifications for Phoenix LiveView.
View Demo - See Toast in action with interactive examples.
Toast is a notification system for Phoenix LiveView that works as a drop-in replacement for your existing flash messages. It provides three ways to show notifications:
- Toast messages - Call
Toast.send_toast()from your LiveView to show rich, interactive notifications - Pipe operation - Use
Toast.put_toast()to pipe toast messages in your socket chain - Flash messages - Your existing
put_flash()calls continue to work, displayed in the same toast style
- 📚 Stackable toasts - Unlike flash messages, display multiple toasts simultaneously
- 🔄 Drop-in replacement - Your existing
put_flash()calls render as beautiful toasts - ✨ Beautiful by default - Inspired by Sonner's elegant design, looks great out of the box
- 🎨 Framework agnostic styling - Ships with CSS, works with any CSS framework or custom styles
- ⚙️ Highly customizable - Themes, positions, animations, icons, actions - all configurable
- 🚀 Server-controlled - Full power of LiveView, no client-side state to manage
- 📦 Self-contained - CSS and JS included, no build step or npm required
- 🎯 Zero configuration - Works out of the box with sensible defaults
- 🪶 Tiny footprint - Single JS hook, no external dependencies
Add toast to your list of dependencies in mix.exs:
def deps do
[
{:toast, "~> 0.3.0"}
]
endThese steps match a new Phoenix 1.8 app. Paths assume the default assets/js/app.js and assets/css/app.css.
In assets/js/app.js, import the hook and add it to your existing LiveSocket hooks. Keep the other options as they are:
import Toast from "../../deps/toast/assets/js/toast.js"
const liveSocket = new LiveSocket("/live", Socket, {
longPollFallbackMs: 2500,
params: {_csrf_token: csrfToken},
hooks: {...colocatedHooks, Toast},
})On Phoenix 1.7 there is no hooks option yet, so add hooks: {Toast}.
In assets/css/app.css, import the stylesheet directly after the @import "tailwindcss" line:
@import "tailwindcss" source(none);
@import "../../deps/toast/assets/css/toast.css";If you use daisyUI (the Phoenix 1.8 default), its toast class clashes with Toast's. Add exclude: toast; to your existing daisyUI plugin block:
@plugin "daisyui/packages/bundle/daisyui" {
themes: false;
exclude: toast;
}Toast.toast_group is a LiveComponent, so it must be rendered inside a LiveView. Do not put it in root.html.heex: the root layout is rendered outside the LiveView and every page fails with cannot convert component Toast.LiveComponent ... to HTML.
Phoenix 1.8: in lib/my_app_web/components/layouts.ex, replace <.flash_group flash={@flash} /> in Layouts.app with Toast.toast_group. Set connection_toasts to keep the "We can't find the internet" and "Something went wrong!" messages from Phoenix's flash_group (the #client-error and #server-error flashes):
attr :flash, :map, required: true, doc: "the map of flash messages"
attr :live, :boolean, default: true, doc: "set to false in controller templates"
def app(assigns) do
~H"""
<%!-- generated <header> and <main> stay as they are --%>
<Toast.toast_group :if={@live} flash={@flash} connection_toasts={true} />
<Toast.flash_group :if={!@live} flash={@flash} />
"""
endController templates that use Layouts.app are not LiveViews, so they pass live={false} and get a plain flash list in the same style. If you set position, theme or rich_colors on Toast.toast_group, pass the same values to Toast.flash_group:
<Layouts.app flash={@flash} live={false}>
...
</Layouts.app>Without live={false}, a controller page that uses Layouts.app fails with the same error as above. On controller pages the flash close button does nothing, because the JavaScript hook only runs in a LiveView.
The generated home page, lib/my_app_web/controllers/page_html/home.html.heex, doesn't use Layouts.app and renders <Layouts.flash_group flash={@flash} /> itself. Replace that line with <Toast.flash_group flash={@flash} />. Phoenix's own flash relies on daisyUI's toast class, which the exclude: toast; line above removes.
Phoenix 1.7: app.html.heex is the layout for both LiveViews and controllers, and only LiveViews have @socket. Replace <.flash_group flash={@flash} /> with:
<Toast.toast_group :if={assigns[:socket]} flash={@flash} connection_toasts={true} />
<Toast.flash_group :if={!assigns[:socket]} flash={@flash} />Send toasts from your LiveView event handlers:
def handle_event("save", _params, socket) do
{:noreply,
socket
|> assign(:saved, true)
|> Toast.put_toast(:success, "Changes saved!")}
end
def handle_event("delete", _params, socket) do
{:noreply,
socket
|> Toast.put_toast(:error, "Failed to delete", duration: 5000)}
end
# put_toast is stored in the flash, so it is still shown after navigating
def handle_event("create", _params, socket) do
{:noreply,
socket
|> Toast.put_toast(:success, "Item created!")
|> push_navigate(to: ~p"/items")}
end
# Or without piping (shown on the current LiveView only)
def handle_event("notify", _params, socket) do
Toast.send_toast(:info, "Notification sent!")
{:noreply, socket}
endNotes on put_toast and navigation:
- On
redirect/2the flash travels in a cookie, which browsers drop above about 4KB. A large toast can push it over, and then no flash messages show on the next page. - From a LiveComponent,
put_toastshows the toast right away but doesn't carry it across navigation. Call it in the parent LiveView for that.
Toast continues to render your app's flash messages in the same style as toasts:
# In a LiveView
def handle_event("save", _params, socket) do
{:noreply, put_flash(socket, :success, "Settings updated!")}
end
# In a controller
def create(conn, params) do
conn
|> put_flash(:success, "Item created successfully!")
|> redirect(to: ~p"/items")
endFlash messages don't auto-dismiss. They stay until the user closes them. Use put_toast or send_toast for messages that should go away on their own.
Toast comes with 6 built-in types, each with its own icon and color:
:info- Blue informational messages:success- Green success messages:error- Red error messages:warning- Yellow warning messages:loading- Loading state with spinner:default- Default neutral style
Configure the toast container with these attributes:
<Toast.toast_group
flash={@flash}
position="bottom-right"
theme="light"
rich_colors={false}
max_toasts={3}
animation_duration={400}
expanded={false}
/>| Option | Type | Default | Description |
|---|---|---|---|
id |
string | "toast-group" |
DOM id of the toast list. Connection toasts use "<id>-client-error" and "<id>-server-error". Render one toast group per page |
position |
string | "bottom-right" |
Position of toasts: "top-left", "top-center", "top-right", "bottom-left", "bottom-center", "bottom-right" |
theme |
string | "light" |
"light" or "dark" always use that theme. "system" follows the page theme, then the OS (see Themes) |
rich_colors |
boolean | false |
Use more vibrant colors for toast types |
max_toasts |
integer | 3 |
Maximum number of toasts shown. Older toasts beyond the limit are removed |
animation_duration |
integer | 400 |
Duration of animations in milliseconds |
connection_toasts |
boolean | false |
Show an error toast while the LiveView is disconnected |
client_error_title |
string | "We can't find the internet" |
Title when the client loses its connection |
client_error_message |
string | "Attempting to reconnect" |
Message when the client loses its connection |
server_error_title |
string | "Something went wrong!" |
Title when the server crashes |
server_error_message |
string | "Hang in there while we get back on track" |
Message when the server crashes |
expanded |
boolean | false |
Always show toasts expanded instead of stacked |
theme="light" and theme="dark" ignore the page around them.
theme="system" follows the page when it sets a theme, and the OS otherwise:
- A
data-theme="dark"attribute ordarkclass on an ancestor (such as<html>) gives dark toasts. - A
data-theme="light"attribute on an ancestor gives light toasts, even when the OS is dark. - With neither, toasts follow the OS setting via
prefers-color-scheme.
This works with the light / dark / system toggle in Phoenix 1.8's generated layout, which
sets data-theme on <html> to the chosen or OS theme.
Phoenix's generated layouts show #client-error and #server-error flashes when the
LiveView loses its connection. Set connection_toasts to show these as toasts instead.
If you replaced <.flash_group> as shown in Setup, those flashes are already gone.
Otherwise remove the two <.flash> elements from your layout:
<Toast.toast_group flash={@flash} connection_toasts={true} />They use the same phx-disconnected and phx-connected bindings as Phoenix, so they
show while disconnected and hide on reconnect. Pass your own text to translate them:
<Toast.toast_group
flash={@flash}
connection_toasts={true}
client_error_title={gettext("We can't find the internet")}
client_error_message={gettext("Attempting to reconnect")}
server_error_title={gettext("Something went wrong!")}
server_error_message={gettext("Hang in there while we get back on track")}
/>send_toast and put_toast take the same options:
Toast.send_toast(:success, "Message",
title: "Success!",
description: "Additional details here",
duration: 10_000,
close_button: true,
important: true,
action: %{
label: "Undo",
event: "undo_action",
params: %{id: 123}
}
)| Option | Type | Default | Description |
|---|---|---|---|
id |
string | random | Toast id. Set it to update or clear the toast later |
title |
string | nil |
Toast title (displayed above message) |
description |
string | nil |
Additional text (displayed below message) |
duration |
integer | 6000 |
Auto-dismiss time in milliseconds (0 = no auto-dismiss) |
close_button |
boolean | true |
Show close button |
important |
boolean | false |
Use aria-live="assertive" for screen readers |
icon |
string | nil |
Custom SVG icon HTML |
action |
map | nil |
Action button configuration |
Actions can be configured with:
action: %{
label: "Button text", # Required
event: "event_name", # Event to send to LiveView
params: %{key: "value"}, # Optional parameters
inline: true # Show as inline button (default: false)
}Clicking the button closes the toast and sends {event, params} to the LiveView that sent the toast. Handle it in handle_info/2. The event arrives as an atom and params has string keys:
def handle_event("delete", %{"id" => id}, socket) do
Toast.send_toast(:info, "Item deleted",
action: %{label: "Undo", event: "undo_delete", params: %{id: id}}
)
{:noreply, socket}
end
def handle_info({:undo_delete, %{"id" => id}}, socket) do
# restore the item
{:noreply, socket}
endProvide custom SVG icons:
Toast.send_toast(:info, "Custom icon",
icon: ~s(<svg>...</svg>)
)Toast supports rendering raw HTML in messages, titles, and descriptions using Phoenix.HTML.raw/1:
# Basic HTML formatting
Toast.send_toast(:info, Phoenix.HTML.raw("<strong>Bold</strong> and <em>italic</em> text"))
# Rich HTML content
Toast.send_toast(:success, Phoenix.HTML.raw("Payment processed"),
title: Phoenix.HTML.raw("Transaction <em>Complete</em>"),
description: Phoenix.HTML.raw("ID: <code>TXN-12345</code>")
)
# Mixed content (some fields escaped, some raw)
Toast.send_toast(:info, "This is escaped: <script>",
description: Phoenix.HTML.raw("This is <strong>HTML</strong>")
)Phoenix.HTML.raw/1 with trusted content. Never render user-generated HTML without proper sanitization as it can lead to XSS vulnerabilities.
Update a toast after it's displayed:
# Create with custom ID. duration: 0 keeps it open until it is updated
Toast.send_toast(:loading, "Processing...", id: "upload-toast", duration: 0)
# Later, update it
Toast.update_toast("upload-toast",
type: :success,
message: "Upload complete!",
duration: 3000
)The toast is re-rendered in place. A new duration restarts its timer. Updating an ID that is no longer showing does nothing.
Remove toasts programmatically:
# Clear specific toast
Toast.clear_toast("toast-id")
# Clear all toasts
Toast.clear_all_toasts()When multiple toasts are displayed:
- Toasts stack with the newest on top
- Hovering expands the stack to show all toasts and pauses every toast's timer. Each one resumes with the time it had left when the pointer leaves
- Set
expanded={true}to turn off stacking and always show toasts expanded - Only the newest
max_toastsare kept. Older ones fade out and are removed - Toasts animate smoothly when added/removed
Each toast reads its colors from CSS custom properties: --toast-bg, --toast-color, --toast-border and --toast-icon-color. Override them per type in your app.css, after the Toast import:
.live-toast-group .toast-success {
--toast-bg: #10b981;
--toast-color: white;
--toast-border: #059669;
--toast-icon-color: white;
}Set the animation speed with the animation_duration attribute, not CSS.
Toast renders no inline style attributes or <style> elements, so it works under a strict CSP such as style-src 'self' without 'unsafe-inline'. Load toast.css as part of your own stylesheet. Any HTML you pass in yourself (custom icons, Phoenix.HTML.raw/1 content) must also avoid inline styles.
Toast was heavily influenced by:
- Sonner - An opinionated toast component for React.
- LiveToast - A beautiful drop-in replacement for the Phoenix Flash system.
Contributions are welcome! Please feel free to submit a Pull Request.
MIT License - see LICENSE file for details.