A succinct message that is displayed temporarily.
<Toast.provider duration={8000} swipe_direction="right" label="Notification">
<button
type="button"
id="docs-toast-show-btn"
class="DemoButton large violet"
onclick="const toast = document.getElementById('docs-toast-primitive'); if (!toast) return; toast.hidden = false; toast.dataset.state = 'open'; toast.dispatchEvent(new CustomEvent('essence:toast:open', {bubbles: true}));"
>
Add to calendar
</button>
<Toast.viewport id="docs-toast-viewport" class="DemoToastViewport">
<Toast.root
id="docs-toast-primitive"
duration={3000}
type="foreground"
default_open={false}
class="DemoToastRoot"
>
<Toast.title class="DemoToastTitle">Scheduled: Catch up</Toast.title>
<Toast.description class="DemoToastDescription">Friday at 5:30 PM</Toast.description>
<Toast.action
alt_text="Goto schedule to undo"
class="DemoToastAction DemoButton small green"
>
Undo
</Toast.action>
<Toast.close class="DemoToastClose" aria-label="Close">×</Toast.close>
</Toast.root>
</Toast.viewport>
</Toast.provider>
/* reset */
button {
all: unset;
}
.DemoToastViewport {
--viewport-padding: 25px;
position: fixed;
bottom: 0;
right: 0;
display: flex;
flex-direction: column;
padding: var(--viewport-padding);
gap: 10px;
width: 390px;
max-width: 100vw;
margin: 0;
list-style: none;
z-index: 2147483647;
outline: none;
}
.DemoToastRoot {
background-color: white;
border-radius: 6px;
box-shadow:
hsl(206 22% 7% / 35%) 0px 10px 38px -10px,
hsl(206 22% 7% / 20%) 0px 10px 20px -15px;
padding: 15px;
display: grid;
grid-template-areas: "title action" "description action";
grid-template-columns: auto max-content;
column-gap: 15px;
align-items: center;
}
.DemoToastRoot[data-state="open"] {
animation: slideIn 150ms cubic-bezier(0.16, 1, 0.3, 1);
}
.DemoToastRoot[data-state="closed"] {
animation: hide 100ms ease-in;
}
.DemoToastRoot[data-swipe="move"] {
transform: translateX(var(--radix-toast-swipe-move-x));
}
.DemoToastRoot[data-swipe="cancel"] {
transform: translateX(0);
transition: transform 200ms ease-out;
}
.DemoToastRoot[data-swipe="end"] {
animation: swipeOut 100ms ease-out;
}
@keyframes hide {
from {
opacity: 1;
}
to {
opacity: 0;
}
}
@keyframes slideIn {
from {
transform: translateX(calc(100% + var(--viewport-padding)));
}
to {
transform: translateX(0);
}
}
@keyframes swipeOut {
from {
transform: translateX(var(--radix-toast-swipe-end-x));
}
to {
transform: translateX(calc(100% + var(--viewport-padding)));
}
}
.DemoToastTitle {
grid-area: title;
margin-bottom: 5px;
font-weight: 500;
color: var(--slate-12);
font-size: 15px;
}
.DemoToastDescription {
grid-area: description;
margin: 0;
color: var(--slate-11);
font-size: 13px;
line-height: 1.3;
}
.DemoToastAction {
grid-area: action;
}
.DemoButton {
display: inline-flex;
align-items: center;
justify-content: center;
border-radius: 4px;
font-weight: 500;
user-select: none;
}
.DemoButton.small {
font-size: 12px;
padding: 0 10px;
line-height: 25px;
height: 25px;
}
.DemoButton.large {
font-size: 15px;
padding: 0 15px;
line-height: 35px;
height: 35px;
}
.DemoButton.violet {
background-color: white;
color: var(--violet-11);
box-shadow: 0 2px 10px var(--black-a7);
}
.DemoButton.violet:hover {
background-color: var(--mauve-3);
}
.DemoButton.violet:focus {
box-shadow: 0 0 0 2px black;
}
.DemoButton.green {
background-color: var(--green-2);
color: var(--green-11);
box-shadow: inset 0 0 0 1px var(--green-7);
}
.DemoButton.green:hover {
box-shadow: inset 0 0 0 1px var(--green-8);
}
.DemoButton.green:focus {
box-shadow: 0 0 0 2px var(--green-8);
}
.DemoToastClose {
all: unset;
font-family: inherit;
border-radius: 100%;
height: 25px;
width: 25px;
display: inline-flex;
align-items: center;
justify-content: center;
color: var(--violet-11);
background-color: var(--gray-3);
}
.DemoToastClose:hover {
background-color: var(--violet-4);
}
.DemoToastClose:focus {
box-shadow: 0 0 0 2px var(--violet-7);
}
Anatomy
Import the component.
<Toast.provider>
<Toast.root>
<Toast.title />
<Toast.description />
<Toast.action />
<Toast.close />
</Toast.root>
<Toast.viewport />
</Toast.provider>
Anatomy
-
Provider - Wraps toasts and the viewport. Supplies default duration, swipe direction, and label.
-
Viewport - The fixed area where toasts appear.
-
Root - An individual toast that auto-dismisses.
-
Title - An optional title for the toast.
-
Description - The toast message.
-
Action - An optional action button. Requires `alt_text` for screen readers.
-
Close - A button that dismisses the toast before its duration elapses.
API Reference
Provider
The provider that wraps your toasts and toast viewport. It usually wraps the application.
| Prop | Type | Default | Description |
|---|---|---|---|
duration
|
integer
|
5000
|
|
label
|
string
|
"Notification"
|
|
swipe_direction
|
string
|
"right"
|
duration
integer
Default
5000
label
string
Default
"Notification"
swipe_direction
string
Default
"right"
Viewport
The fixed area where toasts appear. Users can jump to the viewport by pressing the configured hotkey (F8 by default). It is up to you to ensure the discoverability of the hotkey for keyboard users.
| Prop | Type | Default | Description |
|---|---|---|---|
hotkey
|
string
|
"F8"
|
|
id
|
string
|
nil
|
|
label
|
string
|
"Notifications ({hotkey})"
|
hotkey
string
Default
"F8"
id
string
Default
nil
label
string
Default
"Notifications ({hotkey})"
Root
The toast that automatically closes. It should not be held open to acquire a user response—use an AlertDialog styled as a toast when a response is required.
Use open with on_open_change for controlled state in LiveView:
<Toast.root id="saved-toast" open={@toast_open} on_open_change="toast_open_change">
…
</Toast.root>
def handle_event("toast_open_change", %{"open" => open}, socket) do
{:noreply, assign(socket, toast_open: open)}
end
The ToastRoot hook pauses the dismiss timer on pointer enter and focus, resumes on leave/blur, closes on Escape when focused, and dispatches essence:toast:open / essence:toast:close custom events for imperative control.
| Prop | Type | Default | Description |
|---|---|---|---|
default_open
|
boolean
|
true
|
|
duration
|
integer
|
nil
|
|
id
|
string
|
—
|
|
on_open_change
|
string
|
nil
|
|
open
|
boolean
|
nil
|
|
type
|
string
|
"foreground"
|
default_open
boolean
Default
true
duration
integer
Default
nil
id
string
Default
—
on_open_change
string
Default
nil
open
boolean
Default
nil
type
string
Default
"foreground"
Data attributes
| Attribute | Values | Description |
|---|---|---|
[data-state]
|
open | closed
|
Reflects the open state. |
[data-swipe]
|
start | move | cancel | end
|
Present during swipe interactions (CSS-ready; pointer swipe handling is not yet implemented in the hook). |
[data-swipe-direction]
|
up | down | left | right
|
Synced from the provider for styling swipe animations. |
CSS variables for swipe animations (set when swipe handling is active):
| CSS variable | Description |
| --- | --- |
| --radix-toast-swipe-move-x | Horizontal offset while swiping |
| --radix-toast-swipe-move-y | Vertical offset while swiping |
| --radix-toast-swipe-end-x | Final horizontal offset after swipe |
| --radix-toast-swipe-end-y | Final vertical offset after swipe |
Title
An optional title for the toast.
| Prop | Type | Default | Description |
|---|
Description
The toast message.
| Prop | Type | Default | Description |
|---|
Action
An action that is safe to ignore so users are not expected to complete tasks with unexpected side effects as a result of a time limit.
When obtaining a user response is necessary, portal an AlertDialog styled as a toast into the viewport instead.
| Prop | Type | Default | Description |
|---|---|---|---|
alt_text
|
string
|
—
|
alt_text
string
Default
—
Close
A button that allows users to dismiss the toast before its duration has elapsed.
| Prop | Type | Default | Description |
|---|
Examples
Custom hotkey
Override the default hotkey on the viewport. Essence accepts a single key name (for example "F8" or "T"); modifier combinations are not yet supported.
<Toast.provider>
…
<Toast.viewport hotkey="T" label="Notifications (T)" />
</Toast.provider>
Press the configured key to focus the viewport region.
Custom duration
Customise the duration of a toast to override the provider value.
<Toast.root id="saved" duration={3000}>
<Toast.description>Saved!</Toast.description>
</Toast.root>
Duplicate toasts
When a toast must appear every time a user clicks a button, use LiveView state to render multiple instances of the same toast:
<Toast.provider>
<form phx-submit="save">
<button type="submit">Save</button>
</form>
<Toast.viewport>
<%= for id <- @toast_ids do %>
<Toast.root id={id} default_open={true}>
<Toast.description>Saved!</Toast.description>
</Toast.root>
<% end %>
</Toast.viewport>
</Toast.provider>
def handle_event("save", _params, socket) do
id = "toast-#{System.unique_integer([:positive])}"
{:noreply, update(socket, :toast_ids, &[id | &1])}
end
Animating swipe gesture
Combine --radix-toast-swipe-move-[x|y] and --radix-toast-swipe-end-[x|y] CSS variables with data-swipe="[start|move|cancel|end]" attributes to animate a swipe-to-close gesture. The stylesheet below matches the demo CSS; the ToastRoot hook currently syncs data-swipe-direction from the provider—pointer swipe events that set data-swipe are not yet implemented.
<Toast.provider swipe_direction="right">
<Toast.root id="swipe-toast" class="DemoToastRoot">…</Toast.root>
<Toast.viewport />
</Toast.provider>
.DemoToastRoot[data-swipe="move"] {
transform: translateX(var(--radix-toast-swipe-move-x));
}
.DemoToastRoot[data-swipe="cancel"] {
transform: translateX(0);
transition: transform 200ms ease-out;
}
.DemoToastRoot[data-swipe="end"] {
animation: slideRight 100ms ease-out;
}
@keyframes slideRight {
from {
transform: translateX(var(--radix-toast-swipe-end-x));
}
to {
transform: translateX(100%);
}
}
Accessibility
Adheres to the aria-live requirements.
Sensitivity
Control the sensitivity of the toast for screen readers using the type attribute.
For toasts that are the result of a user action, choose foreground. Toasts generated from background tasks should use background.
Foreground
Foreground toasts are announced immediately. Assistive technologies may choose to clear previously queued messages when a foreground toast appears. Try to avoid stacking distinct foreground toasts at the same time.
Background
Background toasts are announced at the next graceful opportunity, for example when the screen reader has finished reading its current sentence. They do not clear queued messages so overusing them can be perceived as a laggy user experience for screen reader users when used in response to a user interaction.
<Toast.root id="foreground-toast" type="foreground">
<Toast.description>File removed successfully.</Toast.description>
<Toast.close aria-label="Close">Dismiss</Toast.close>
</Toast.root>
<Toast.root id="background-toast" type="background">
<Toast.description>We've just released Essence UI 1.0.</Toast.description>
<Toast.close aria-label="Close">Dismiss</Toast.close>
</Toast.root>
Alternative action
Use the alt_text attribute on Toast.action to instruct an alternative way of actioning the toast to screen reader users.
<Toast.root id="upgrade-toast" type="background">
<Toast.title>Upgrade Available!</Toast.title>
<Toast.description>We've just released Essence UI 1.0.</Toast.description>
<Toast.action alt_text="Goto account settings to upgrade">Upgrade</Toast.action>
<Toast.close aria-label="Close">Dismiss</Toast.close>
</Toast.root>
Close icon button
When providing an icon (or font icon), remember to label it correctly for screen reader users.
<Toast.root id="saved-icon-toast" type="foreground">
<Toast.description>Saved!</Toast.description>
<Toast.close aria-label="Close">
<span aria-hidden="true">×</span>
</Toast.close>
</Toast.root>
Keyboard Interactions
Keyboard Interactions
| Key | Description |
|---|---|
F8
|
Focuses the toast viewport (or the key configured via `hotkey`). |
Tab
|
Moves focus to the next focusable element. |
Shift + Tab
|
Moves focus to the previous focusable element. |
Space
|
When focus is on a `Toast.action` or `Toast.close`, activates the control. |
Enter
|
When focus is on a `Toast.action` or `Toast.close`, activates the control. |
Escape
|
When focus is on a toast, closes the toast. |
Custom APIs
Abstract parts
Create your own API by abstracting the primitive parts into your own component.
Usage
<.toast id="upgrade" title="Upgrade available" content="We've just released Essence UI 1.0!">
Upgrade
</.toast>
Implementation
def toast(assigns) do
~H"""
<Toast.root id={@id} {@rest}>
<Toast.title :if={@title}>{@title}</Toast.title>
<Toast.description>{@content}</Toast.description>
<Toast.action :if={@inner_block != []} alt_text={@alt_text || "Perform action"}>
{render_slot(@inner_block)}
</Toast.action>
<Toast.close aria-label="Close">
<span aria-hidden="true">×</span>
</Toast.close>
</Toast.root>
"""
end
Imperative API
Create your own imperative API to allow toast duplication if preferred.
Usage
Track toast count in LiveView and expose a phx-click that increments it:
<Toast.provider>
<button type="button" phx-click="show_saved_toast">Save</button>
<Toast.viewport>
<%= for id <- @saved_toast_ids do %>
<Toast.root id={id} default_open={true}>
<Toast.description>Saved successfully!</Toast.description>
<Toast.close aria-label="Close">Dismiss</Toast.close>
</Toast.root>
<% end %>
</Toast.viewport>
</Toast.provider>
Implementation
def handle_event("show_saved_toast", _params, socket) do
id = "saved-#{System.unique_integer([:positive])}"
{:noreply, assign(socket, saved_toast_ids: [id | socket.assigns.saved_toast_ids])}
end