Skip to main content

Theme config

Theme mode

Color palette

Grays

Border radii/radiuses/radiopedes/you know
Border radius
Field border radius
Button border radius
All
Components
Guides
API
Recent

Components

Toast

Non-interruptive and stackable notifications. They pop up like... toast.

Full support Supported since v135. Full support Supported since v144. Full support Supported since v26.2.

How it works

Toasts are managed by a global container. Trigger them either completely with HTML (using invoker commands) or with JavaScript.

Structure lives in HTML via a default <template id="toast-template">. CSS owns the lifetime through attr(data-duration type(<time>)). JS only clones the template, fills the structural slots ([data-toast-title], [data-toast-description], [data-toast-icon], [data-toast-close]) using textContent, and removes the toast on animationend. Provide your own <template> with the same slot markers and reference it via data-template on the trigger to override the default look.

HTML

Use commandfor="toast-manager" and command="--show-toast" on a button. The data-title attribute will be used as the message.

---
import { Button } from "@opui/astro"
---
<Button
commandfor="toast-manager"
command="--show-toast"
data-title="Default Notification"
>
Show Default Toast
</Button>
<button
commandfor="toast-manager"
command="--show-toast"
data-title="Default Notification"
class="button"
>
Show Default Toast
</button>

JavaScript

This is arguably the most common way. After some kind of fetch you might want to display some kind of message.

Use the showToast helper or by dispatching a CommandEvent.

// Using the helper
window.showToast({
title: 'Triggered from JS!',
description: 'With an optional description',
severity: 'success',
duration: '3000ms'
});
// Or using native CommandEvent
const btn = document.createElement('button');
btn.setAttribute('data-title', 'Triggered from JS!');
btn.setAttribute('data-description', 'With an optional description');
btn.setAttribute('data-severity', 'success');
btn.setAttribute('data-duration', '3000ms');
document.getElementById('toast-manager').dispatchEvent(
new CommandEvent('command', {
command: '--show-toast',
source: btn
})
);
<script type="module">
function e() {
const t = document.getElementById("js-trigger");
t &&
t.addEventListener("click", () => {
window.showToast &&
window.showToast({
title: "Triggered from JS!",
description: "With an optional description",
severity: "success",
duration: "3000ms",
});
});
}
e();
document.addEventListener("astro:after-swap", e);
</script>
<button id="js-trigger" class="button">Trigger from JS</button>

Severities

Use the data-severity attribute to change the appearance of the toast.

---
import { Button } from "@opui/astro"
---
<Button
commandfor="toast-manager"
command="--show-toast"
data-title="Success!"
data-severity="success"
variant="filled"
class="green"
>
Success
</Button>
<Button
commandfor="toast-manager"
command="--show-toast"
data-title="Something went wrong"
data-severity="critical"
variant="filled"
class="red"
>
Error
</Button>
<Button
commandfor="toast-manager"
command="--show-toast"
data-title="Did you know?"
data-severity="info"
variant="filled"
class="blue"
>
Info
</Button>
<button
commandfor="toast-manager"
command="--show-toast"
data-title="Success!"
data-severity="success"
class="button filled green"
>
Success</button
><button
commandfor="toast-manager"
command="--show-toast"
data-title="Something went wrong"
data-severity="critical"
class="button filled red"
>
Error</button
><button
commandfor="toast-manager"
command="--show-toast"
data-title="Did you know?"
data-severity="info"
class="button filled blue"
>
Info
</button>

Title + description

Use data-title for a single-line toast, or add data-description for a two-line toast with additional context.

---
import { Button } from "@opui/astro"
---
<Button
commandfor="toast-manager"
command="--show-toast"
data-title="Title only"
>
Title only
</Button>
<Button
commandfor="toast-manager"
command="--show-toast"
data-title="Title with description"
data-description="This is additional context information"
>
Title + description
</Button>
<button
commandfor="toast-manager"
command="--show-toast"
data-title="Title only"
class="button"
>
Title only</button
><button
commandfor="toast-manager"
command="--show-toast"
data-title="Title with description"
data-description="This is additional context information"
class="button"
>
Title + description
</button>

Duration

Control how long the toast stays visible using data-duration. Supports CSS time units such as ms or s.

---
import { Button } from "@opui/astro"
---
<Button
commandfor="toast-manager"
command="--show-toast"
data-title="I disappear quickly"
data-duration="1500ms"
>
1.5s Toast
</Button>
<Button
commandfor="toast-manager"
command="--show-toast"
data-title="I stay for a while"
data-duration="10s"
>
10s Toast
</Button>
<button
commandfor="toast-manager"
command="--show-toast"
data-title="I disappear quickly"
data-duration="1500ms"
class="button"
>
1.5s Toast</button
><button
commandfor="toast-manager"
command="--show-toast"
data-title="I stay for a while"
data-duration="10s"
class="button"
>
10s Toast
</button>

Anatomy

1. Container (Manager)
2. Individual Toast Body
3. Content Area
4. Close Button (Optional)

Notification message content

API

The Toast component does not accept any props as it is a global manager container.

Browser support

Full support Supported since v135. Full support Supported since v144. Full support Supported since v26.2.

See also the full browser support guide.

Installation

@layer components.extended {
:where(#toast-manager) {
background: none;
block-size: auto;
border: none;
display: none;
flex-direction: column-reverse;
gap: var(--size-3);
inline-size: auto;
inset: auto var(--size-4) var(--size-4) auto;
margin: 0;
max-inline-size: calc(100vi - var(--size-8));
overflow: visible;
padding: 0;
pointer-events: none;
position: fixed;
z-index: 1000;
&:popover-open {
display: flex;
}
&::backdrop {
display: none;
}
/* Optional cap on visible toasts. Older toasts beyond this depth are hidden
but still in the DOM so they animate cleanly when newer ones are removed. */
/* > .toast:nth-last-child(n + 6) { display: none; } */
}
:where(.toast) {
--_anim-enter: 0.3s;
--_anim-exit: 0.3s;
align-items: center;
animation:
toast-enter var(--_anim-enter) var(--ease-out-3) both,
toast-hold attr(data-duration type(<time>), 5s) linear both,
toast-exit var(--_anim-exit) var(--ease-in-3) both;
animation-delay:
0s,
var(--_anim-enter),
calc(var(--_anim-enter) + attr(data-duration type(<time>), 5s));
background-color: light-dark(var(--gray-15), var(--gray-2));
border-radius: var(--border-radius);
box-shadow: var(--shadow-3);
color: var(--text-primary-contrast);
display: flex;
gap: var(--size-3);
justify-content: space-between;
min-inline-size: 30ch;
padding: var(--size-3) var(--size-4);
pointer-events: auto;
&:hover {
animation-play-state: paused;
}
&.exiting {
animation: toast-exit var(--_anim-exit) var(--ease-in-3) forwards;
}
.icon {
background: no-repeat center / contain;
block-size: var(--size-4);
flex-shrink: 0;
inline-size: var(--size-4);
}
/* Hide the icon slot when there's no severity to render against. */
&:not([data-severity]) .icon {
display: none;
}
.content {
font-size: var(--font-size-05);
word-break: break-word;
}
.title {
font-weight: 600;
}
.description {
color: oklch(from currentColor l c h / 75%);
font-size: var(--font-size-0);
}
.close-button {
background: none;
border: none;
color: inherit;
cursor: pointer;
display: grid;
opacity: 0.7;
padding: var(--size-1);
place-items: center;
transition: opacity 0.2s;
&:hover {
opacity: 1;
}
}
/* Severity icons via CSS data URIs — keeps icon assets out of JS strings. */
&[data-severity="success"] .icon {
background-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='20' height='20' viewBox='0 0 24 24' fill='none' stroke='%2322c55e' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'><path d='M22 11.08V12a10 10 0 1 1-5.93-9.14'/><path d='m9 11 3 3L22 4'/></svg>");
}
&[data-severity="info"] .icon {
background-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='20' height='20' viewBox='0 0 24 24' fill='none' stroke='%233b82f6' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'><circle cx='12' cy='12' r='10'/><path d='M12 16v-4'/><path d='M12 8h.01'/></svg>");
}
&[data-severity="warning"] .icon {
background-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='20' height='20' viewBox='0 0 24 24' fill='none' stroke='%23f59e0b' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'><path d='m21.73 18-8-14a2 2 0 0 0-3.48 0l-8 14A2 2 0 0 0 4 21h16a2 2 0 0 0 1.73-3'/><path d='M12 9v4'/><path d='M12 17h.01'/></svg>");
}
&[data-severity="critical"] .icon {
background-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='20' height='20' viewBox='0 0 24 24' fill='none' stroke='%23ef4444' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'><circle cx='12' cy='12' r='10'/><path d='m15 9-6 6'/><path d='m9 9 6 6'/></svg>");
}
}
@keyframes toast-enter {
from {
opacity: 0;
translate: var(--size-4) 0;
}
to {
opacity: 1;
translate: 0 0;
}
}
/* Identity keyframe — only purpose is to delay the exit animation by the
author-defined duration. */
@keyframes toast-hold {
from {
opacity: 1;
}
to {
opacity: 1;
}
}
@keyframes toast-exit {
from {
opacity: 1;
scale: 1;
translate: 0 0;
}
to {
opacity: 0;
scale: 0.9;
translate: 0 var(--size-4);
}
}
}