PUI's Flash system provides a full-featured toast notification system with stacking, auto-dismiss, multiple positions, and LiveComponent support. It's built as an enhanced replacement for Phoenix's built-in flash messages.
Import
use PUI
# or
import PUI.Flash
Basic Usage
Add the flash_group component to your layout to enable flash messages:
<PUI.Flash.flash_group flash={@flash} id="flash-basic-example" />
Sending Flash Messages
Send flash messages from your LiveView event handlers:
def handle_event("save", _params, socket) do
PUI.Flash.send_flash("Changes saved successfully!")
{:noreply, socket}
end
Phoenix Preset Toasts
Phoenix flash keys such as :success, :error, :info, and :warning are
automatically rendered as constrained card toasts with a type-colored icon:
def handle_event("save", _params, socket) do
{:noreply, put_flash(socket, :success, "Changes saved!")}
end
def handle_event("delete", _params, socket) do
{:noreply, put_flash(socket, :error, "Could not delete item")}
end
def handle_event("warn", _params, socket) do
{:noreply, put_flash(socket, :warning, "Session expires soon")}
end
def handle_event("notify", _params, socket) do
{:noreply, put_flash(socket, :info, "New update available")}
end
You can also trigger the same preset toast style through send_flash:
PUI.Flash.send_flash(%PUI.Flash.Message{
type: :success,
message: "Connected!"
})
Preset toasts use a bounded card container, wrap long messages, and keep the icon and visible close button in place. Messages sent without a preset type keep the standard flash UI.
With Options
# Success message
PUI.Flash.send_flash(%PUI.Flash.Message{
type: :info,
message: "Item created!",
duration: 3
})
# Error message
PUI.Flash.send_flash(%PUI.Flash.Message{
type: :error,
message: "Failed to save"
})
Custom Content
Send HEEx content in flashes. When message is a HEEx template, the custom
markup overrides the preset toast styling. Plain-string messages with a preset
type still render as the compact built-in toast with a type-colored icon:
PUI.Flash.send_flash(%PUI.Flash.Message{
type: :success,
message: ~H|<div class="flex items-center gap-2">
<.icon name="hero-check-circle" class="size-5" />
<span>Success!</span>
</div>|
})
Custom Flash with Async Update
You can send a custom flash with rich HEEx content and update it later by ID. This is useful for showing progress and then replacing it with a result:
def handle_event("dispatch_ping", _params, socket) do
server = socket.assigns.server
message = ~H"""
<div class="flex items-center gap-2">
<svg
xmlns="http://www.w3.org/2000/svg"
width="24" height="24" viewBox="0 0 24 24"
fill="none" stroke="currentColor" stroke-width="2"
stroke-linecap="round" stroke-linejoin="round"
class="animate-spin text-foreground size-5"
>
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
<path d="M12 6l0 -3" />
<path d="M16.25 7.75l2.15 -2.15" />
<path d="M18 12l3 0" />
<path d="M16.25 16.25l2.15 2.15" />
<path d="M12 18l0 3" />
<path d="M7.75 16.25l-2.15 2.15" />
<path d="M6 12l-3 0" />
<path d="M7.75 7.75l-2.15 -2.15" />
</svg>
<div>Connecting to server...</div>
</div>
"""
PUI.Flash.send_flash(%PUI.Flash.Message{
id: "ping-#{server.id}",
message: message,
duration: -1
})
parent = self()
Task.async(fn ->
message =
case perform_ping(server.id) do
{:ok, %{status: :up}} ->
~H"""
<div class="flex items-center gap-2">
<.icon name="hero-check-circle" class="size-6 text-green-600" />
<div>Server connected</div>
</div>
"""
_ ->
~H"""
<div class="flex items-center gap-2">
<.icon name="hero-x-circle" class="size-6 text-red-600" />
<div>Server unreachable</div>
</div>
"""
end
PUI.Flash.update_flash(parent, %PUI.Flash.Message{
id: "ping-#{server.id}",
message: message,
duration: 5
})
end)
{:noreply, socket}
end
Set duration: -1 to keep the flash open until you explicitly update or dismiss it.
When message is a HEEx template, the custom markup overrides the preset toast
styling. Plain-string messages with a preset type still render as the built-in
card toast with a type-colored icon.
Interactive Demo
Custom Async Flash
Send a custom flash with a spinner, then update it after a simulated async operation completes.
<div class="space-y-4">
<p class="text-sm text-muted-foreground">
Send a custom flash with a spinner, then update it after a simulated async operation completes.
</p>
<.button
phx-click="dispatch_ping"
disabled={@ping_state == :connecting}
class="disabled:opacity-50"
>
<%= if @ping_state == :connecting do %>
<.icon name="hero-arrow-path" class="size-4 mr-2 animate-spin" /> Connecting...
<% else %>
<.icon name="hero-signal" class="size-4 mr-2" /> Dispatch Ping
<% end %>
</.button>
</div>
Positioning
Flash groups support six positions:
<PUI.Flash.flash_group flash={@flash} id="flash-position-top-center" position="top-center" />
<PUI.Flash.flash_group flash={@flash} id="flash-position-top-left" position="top-left" />
<PUI.Flash.flash_group flash={@flash} id="flash-position-top-right" position="top-right" />
<PUI.Flash.flash_group flash={@flash} id="flash-position-bottom-center" position="bottom-center" />
<PUI.Flash.flash_group flash={@flash} id="flash-position-bottom-left" position="bottom-left" />
<PUI.Flash.flash_group flash={@flash} id="flash-position-bottom-right" position="bottom-right" />
The group position is the fallback for Phoenix flash-map messages. A trigger can override it for a single message:
PUI.Flash.send_flash("Copied!", position: "bottom-right")
PUI.Flash.send_flash(%PUI.Flash.Message{
message: "Saved in a different stack",
position: "top-right"
})
Messages with different positions are laid out in independent stacks in the same full-screen viewport.
Interactive Demo
Send Toast
Position: top-center. Count: 0
<div class="space-y-6">
<div class="flex flex-wrap items-center gap-2">
<span class="text-xs font-semibold uppercase tracking-wide text-muted-foreground">
Position
</span>
<div class="flex flex-wrap gap-1.5">
<button
:for={
position <- ~w(top-left top-center top-right bottom-left bottom-center bottom-right)
}
type="button"
phx-click="select_flash_position"
phx-value-position={position}
class={[
"px-3 py-1 text-xs font-medium rounded-full transition-all",
position == @flash_position &&
"bg-primary text-primary-foreground shadow-sm",
position != @flash_position &&
"bg-muted text-muted-foreground hover:bg-accent hover:text-foreground"
]}
>
{position}
</button>
</div>
</div>
<div class="flex flex-wrap items-center gap-3">
<.button phx-click="send_toast">
<.icon name="hero-bell" class="size-4 mr-2" /> Send Toast
</.button>
<.button variant="outline" phx-click="send_preset_toast" phx-value-type="success">
<.icon name="hero-check-circle" class="size-4 mr-2 text-green-600" /> Success
</.button>
<.button variant="outline" phx-click="send_preset_toast" phx-value-type="error">
<.icon name="hero-x-circle" class="size-4 mr-2 text-red-600" /> Error
</.button>
<.button variant="outline" phx-click="send_preset_toast" phx-value-type="warning">
<.icon name="hero-exclamation-triangle" class="size-4 mr-2 text-yellow-600" /> Warning
</.button>
<.button variant="outline" phx-click="send_preset_toast" phx-value-type="info">
<.icon name="hero-information-circle" class="size-4 mr-2 text-blue-600" /> Info
</.button>
<.button variant="outline" phx-click="send_custom_flash">
<.icon name="hero-check-circle" class="size-4 mr-2 text-green-600" /> Custom
</.button>
<.button variant="outline" phx-click="send_positioned_toast">
<.icon name="hero-arrow-down-right" class="size-4 mr-2" /> Override: bottom-right
</.button>
<.button variant="outline" phx-click="toggle_flash_stack">
<.icon name="hero-queue-list" class="size-4 mr-2" />
Stack: {if @flash_stacked, do: "On", else: "Off"}
</.button>
<p class="text-sm text-muted-foreground self-center">
Position: {@flash_position}. Count: {@toast_count}
</p>
</div>
</div>
Stacking
Messages remain expanded by default. Set stacked to collapse them into a
stack; hover or focus any visible stack indicator to expand all messages:
<PUI.Flash.flash_group flash={@flash} id="flash-stacking-example" stacked />
Collapsed stacks show at most three indicators behind the front message; additional messages appear when the stack expands.
Auto-Dismiss
Control auto-dismiss timing (in milliseconds):
<!-- Dismiss after 3 seconds -->
<PUI.Flash.flash_group flash={@flash} id="flash-timeout-short" auto_dismiss={3000} />
<!-- Dismiss after 10 seconds -->
<PUI.Flash.flash_group flash={@flash} id="flash-timeout-long" auto_dismiss={10000} />
Message Limit
Limit the number of visible messages:
<PUI.Flash.flash_group flash={@flash} id="flash-limit-example" limit={3} />
Live Component Mode
Enable LiveComponent mode for richer flash management:
<PUI.Flash.flash_group flash={@flash} id="flash-live-example" live={true} />
Closeable
Control whether flash messages show a close button:
<PUI.Flash.flash_group flash={@flash} id="flash-close-example" show_close={false} />
API Reference
FlashGroup Attributes
| Name | Type | Default | Description |
|---|---|---|---|
flash |
map |
required | The flash map from socket assigns |
live |
boolean |
false |
Enable LiveComponent mode |
limit |
integer |
3 |
Max visible messages |
position |
string |
"top-center" |
Position: "top-left", "top-right", "top-center", "bottom-left", "bottom-right", "bottom-center" |
stacked |
boolean |
false |
Collapse messages into an expandable stack |
auto_dismiss |
integer | false |
5000 |
Auto-dismiss time in ms; false disables it |
show_close |
boolean |
true |
Show close button |
Flash Attributes
| Name | Type | Default | Description |
|---|---|---|---|
id |
string |
— | Flash message ID |
position |
string |
"top-center" |
Position variant |
type |
atom |
:info |
Message type: :info, :success, :warning, :error |
preset |
boolean |
false |
Use preset card toast styling |
class |
string |
"" |
Additional CSS classes |
duration |
integer |
nil |
Message timeout in seconds; -1 disables auto-dismiss |
auto_dismiss |
boolean |
true |
Disable auto-dismiss for this message when false |
dismissable |
boolean |
true |
Allow manual dismissal |
show_close |
boolean |
true |
Show close button |