<button
class="btn-outline"
hx-trigger="click"
hx-get="/fragments/toast/success"
hx-target="#toaster"
hx-swap="beforeend"
>
Toast from backend (with HTMX)
</button>
<button
class="btn-outline"
@click='
$toast({
category: "success",
title: "Success",
description: "A success toast called from the front-end.",
action: {
label: "Dismiss",
click: "open =false"
}
})
'>
Toast from front-end
</button>
Usage
HTML + Javascript
This component requires Javascript.
To use toasts, you will first need to set up the toaster code to the end of your <body>
(see HTML below).
The <template id="toast-template">
bit is only required if you plan on using the $toast()
method (see below).
You will also need to include the Javascript code provided below, load it as an individual file or use the CLI. Some Alpine.js properties are also required on certain elements (e.g. x-bind
, x-data
, @click
).
<div id="toaster" class="toaster"></div>
<template id="toast-template">
<div class="toast" role="status" aria-atomic="true" x-bind="$toastBindings">
<div class="toast-content">
<div class="flex items-center justify-between gap-x-3 p-4 [&>svg]:size-4 [&>svg]:shrink-0 [&>[role=img]]:size-4 [&>[role=img]]:shrink-0 [&>[role=img]>svg]:size-4">
<template x-if="config.icon">
<span aria-hidden="true" role="img" x-html="config.icon"></span>
</template>
<template x-if="!config.icon && config.category === 'success'">
<svg aria-hidden="true" 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="lucide lucide-circle-check-icon lucide-circle-check">
<circle cx="12" cy="12" r="10" />
<path d="m9 12 2 2 4-4" />
</svg>
</template>
<template x-if="!config.icon && config.category === 'error'">
<svg aria-hidden="true" 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="lucide lucide-circle-x-icon lucide-circle-x">
<circle cx="12" cy="12" r="10" />
<path d="m15 9-6 6" />
<path d="m9 9 6 6" />
</svg>
</template>
<template x-if="!config.icon && config.category === 'info'">
<svg aria-hidden="true" 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="lucide lucide-info-icon lucide-info">
<circle cx="12" cy="12" r="10" />
<path d="M12 16v-4" />
<path d="M12 8h.01" />
</svg>
</template>
<template x-if="!config.icon && config.category === 'warning'">
<svg aria-hidden="true" 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="lucide lucide-triangle-alert-icon lucide-triangle-alert">
<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>
</template>
<section class="flex-1 flex flex-col gap-0.5 items-start">
<template x-if="config.title">
<h2 class="font-medium" x-text="config.title"></h2>
</template>
<template x-if="config.description">
<p class="text-muted-foreground" x-text="config.description"></p>
</template>
</section>
<template x-if="config.action || config.cancel">
<footer class="flex flex-col gap-1 self-start">
<template x-if="config.action?.click">
<button type="button" class="btn h-6 text-xs px-2.5 rounded-sm" @click="executeAction(config.action.click)" x-text="config.action.label"></button>
</template>
<template x-if="config.action?.url">
<a :href="config.action.url" class="btn h-6 text-xs px-2.5 rounded-sm" x-text="config.action.label"></a>
</template>
<template x-if="config.cancel?.click">
<button type="button" class="btn-outline h-6 text-xs px-2.5 rounded-sm" @click="executeAction(config.cancel.click)" x-text="config.cancel.label"></button>
</template>
<template x-if="config.cancel?.url">
<a :href="config.cancel.url" class="btn-outline h-6 text-xs px-2.5 rounded-sm" x-text="config.cancel.label"></a>
</template>
</footer>
</template>
</div>
</div>
</div>
</template>
<script>
window.basecoat = window.basecoat || {};
window.basecoat.registerPopover = function (Alpine) {
if (Alpine.components && Alpine.components.popover) return;
Alpine.data("popover", () => ({
open: false,
$trigger: {
"@click"() {
this.open = !this.open;
},
"@keydown.escape.prevent"() {
this.open = false;
this.$refs.trigger.focus();
},
":aria-expanded"() {
return this.open;
},
"x-ref": "trigger",
},
$content: {
"@keydown.escape.prevent"() {
this.open = false;
this.$refs.trigger.focus();
},
":aria-hidden"() {
return !this.open;
},
"x-cloak": "",
},
}));
};
document.addEventListener("alpine:init", () => {
window.basecoat.registerPopover(Alpine);
});
</script>
Once that's in place, we can do one of two things:
- Append a
<div class="toast">
to the toaster. - Use the
$toast()
Alpine.js "magic method".
Append a toast
To display a toast, just append the following to the toaster:
<div class="toast" x-data="toast" x-bind="$toastBindings">
<div class="toast-content">
<p>This is a toast.</p>
</div>
</div>
You can implement the layout you want within the toast, but here is the standard format used in the examples above:
<div
class="toast"
role="status"
aria-atomic="true"
aria-hidden="false"
data-category="success"
x-data="toast({
category: 'success',
duration: null
})"
x-bind="$toastBindings">
<div class="toast-content">
<div class="flex items-center justify-between gap-x-3 p-4 [&>svg]:size-4 [&>svg]:shrink-0 [&>[role=img]]:size-4 [&>[role=img]]:shrink-0 [&>[role=img]>svg]:size-4">
<svg aria-hidden="true" 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="lucide lucide-circle-check-icon lucide-circle-check">
<circle cx="12" cy="12" r="10" />
<path d="m9 12 2 2 4-4" />
</svg>
<section class="flex-1 flex flex-col gap-0.5 items-start">
<h2 class="font-medium">Success</h2>
<p class="text-muted-foreground">A success toast called from the front-end.</p>
</section>
<footer class="flex flex-col gap-1 self-start">
<button type="button" class="btn h-6 text-xs px-2.5 rounded-sm" @click="close()">Dismiss</button>
</footer>
</div>
</div>
</div>
When setting up x-data
, you can pass a config object with `category` (e.g. "success", "error", "info", "warning") and `duration` (in milliseconds). By default, the duration will be 3 seconds, and 5 seconds for errors.
This approach is used in the first example with HTMX: when the button is clicked, we retrieve the a fragment that contains the markup for the toast and append it to #toaster
.
Use $toast()
If you want to display a toast directly from the front-end, you can use the $toast()
Alpine.js magic method. This is what we're doing in the second example above:
<button
class="btn-outline"
@click='
$toast({
category: "success",
title: "Success",
description: "A success toast called from the front-end.",
action: {
label: "Dismiss",
click: "open =false"
}
})
'>
Toast from front-end
</button>
The $toast()
method accepts a config object with the following properties:
duration
(optional): the duration of the toast in milliseconds. By default, the duration will be 3 seconds, or 5 seconds ifcategory
is "error".category
(optional): "success", "error", "info", "warning" or any arbitrary string.title
(optional): the title of the toast.description
(optional): the description of the toast.action
(optional): an object to define an action button:label
: the label of the action button.click
(optional): a string of Javascript to execute when the button is clicked.url
(optional): a URL to link to when the button is clicked.
cancel
(optional): an object to define a cancel button (similar structure to action).
Jinja and Nunjucks
You can use the toaster()
and toast()
Nunjucks or Jinja macros for this component.
{% from "toast.njk" import toaster %}
{{ toaster(
toasts=[
{
type: "success",
title: "Success",
description: "A success toast called from the front-end.",
action: { label: "Dismiss", click: "close()" }
},
{
type: "info",
title: "Info",
description: "An info toast called from the front-end.",
action: { label: "Dismiss", click: "close()" }
}
]
) }}
{% from "toast.njk" import toast %}
{{ toast(
title="Event has been created",
description="Sunday, December 03, 2023 at 9:00 AM",
cancel={ label: "Undo", click: "close()" }
) }}