HTMX notifications
This is a short article about how to implement notification system with HTMX and zero other dependencies. We will use Hx-Trigger to trigger an event which will add a notification element on the screen and remove it after the specified time .
The following are the requirements for the notification system:
- Ability to trigger it from server
- Multiple variants (warning, info, error, ...)
- Automatically dismissed
Let's start by reacting to a event sent from the server. We will use HTMX ability to trigger an event on the client. This is achieved by adding Hx-Trigger
header to a response for a request triggered by HTMX. Following the HTMX docs our response will have the following header
Hx-Trigger: notification-1
On the client side we will register a listener on the document for this event, and show an alert when the listener is triggered.
document.addEventListener("notification-1", (event) => {
alert("Hello world!");
});
You can test this by . As you can see this triggers an alert dialog. If you open the network tab and look at the headers of the response you will indeed see that we including the header which triggers this action.
We now have the ability to trigger code execution on the client from the server, great! But we need to add additional data in the header to include a message. Luckily besides raw event names, HTMX allows us to add a JSON with the event which will be added in event.detail
.
So our Header will look like this
Hx-Trigger: {"notification-2": { "message": "Hello world!"}}
And our client listener
document.addEventListener("notification-2", (event) => {
alert(event.detail.message);
});
With this approach we can pass arbitrary data from server and use it on the client. HTTP headers can have only US-ASCII characters. If you want to add non US-ASCII characters you need to escape them on the server and unescape them on the client.
Let's move onto actually displaying the notification. We will add an empty div
with absolute position to the body so we can insert notifications inside it.
<div id="notifications-container" style="position: fixed; bottom: 10px; right: 10px"></div>
and instead of showing an alert
create a new div
element which will be inserted inside the notifications-container
.
document.addEventListener("notification-3", (event) => {
const container = document.getElementById("notifications-container");
const element = document.createElement("div");
const title = document.createElement("p");
title.innerHTML = event.detail.title;
const description = document.createElement("small");
description.innerHTML = event.detail.description;
element.appendChild(title);
element.appendChild(description);
container.appendChild(element);
});
Let's . Perfect, let's the notifications and add a little styling and try .
CSS styles
.notifications-container {
position: fixed;
bottom: 10px;
right: 10px;
display: flex;
flex-flow: column;
gap: 10px;
max-width: 24rem;
width: fit-content;
align-items: flex-end;
}
.notification {
box-shadow: 10px 5px 5px black;
padding: .75rem;
background-color: #142e05;
border: 2px solid #16a34a;
border-radius: .5rem;
overflow: hidden;
min-width: 18rem;
width: fit-content;
position: relative;
p {
font-weight: 600;
margin-bottom: 10px;
color: #cdf89e;
}
small {
font-weight: 300;
color: #cdf89e;
}
}
The only thing left is to dismiss the notification after specified time has elapsed. We will use setTimeout
and an addition attribute on the detail
object to get the time in milliseconds.
Our final header will look like this:
Hx-Trigger: {"notification-5": { "message": string, "description": string, "duration": number }}
And we add setTimeout
at the end of our listener.
setTimeout(() => {
element.remove();
}, event.detail.duration);
It , but needs a little bit of animations in my opinion.
Here is the final code with additional styling, animations and a little bit of refactoring.
.notifications-container {
position: fixed;
bottom: 10px;
right: 10px;
display: flex;
flex-flow: column;
gap: 10px;
max-width: 24rem;
width: fit-content;
align-items: flex-end;
}
.notification {
position: relative;
overflow: hidden;
width: fit-content;
min-width: 18rem;
padding: .75rem;
border: 2px solid #16a34a;
border-radius: .5rem;
box-shadow: 10px 5px 5px black;
background-color: #142e05;
transition: opacity 0.3s linear,
transform 0.3s cubic-bezier(0.4, 0, 0.2, 1);
p {
font-weight: 600;
margin-bottom: 10px;
color: #cdf89e;
}
small {
font-weight: 300;
color: #cdf89e;
}
&.hidden-enter {
transform: translateY(100%);
}
&.hidden-exit {
transform: translateX(100%);
}
:last-child {
position: absolute;
bottom: 0;
left: 0;
width: 100%;
height: 10px;
background-color: #16a34a;
}
}
function showNotification(titleText, descriptionText, duration) {
const container = document.getElementById("notifications-container");
const notification = document.createElement("div");
notification.classList.add("notification", "hidden-enter");
const title = document.createElement("p");
title.innerHTML = titleText;
const description = document.createElement("small");
description.innerHTML = descriptionText;
const progressBar = document.createElement("div");
progressBar.classList.add("notification-progress");
progressBar.style.setProperty("width", "0%");
progressBar.style.setProperty("-webkit-transition", `width ${duration}ms linear`);
progressBar.style.setProperty("-moz-transition", `width ${duration}ms linear`);
progressBar.style.setProperty("-o-transition", `width ${duration}ms linear`);
progressBar.style.setProperty("transition", `width ${duration}ms linear`);
notification.appendChild(title);
notification.appendChild(description);
notification.appendChild(progressBar);
container.appendChild(notification);
setTimeout(() => {
notification.style.opacity = 100;
notification.classList.remove("hidden-enter");
progressBar.style.setProperty("width", "100%");
}, 10);
setTimeout(() => {
notification.style.opacity = 0;
notification.classList.add("hidden-exit");
setTimeout(() => {
notification.remove();
}, 300);
}, duration + 100);
}
document.addEventListener("notification-6", (event) => {
console.log(event.detail);
showNotification(event.detail.title, event.detail.description, event.detail.duration);
});
Updates:
- Fixed typo and updated statement about HTTP header encoding, thanks Trick_Ad_3234