Modern Toasts Using the Native Popover API
The progress in browser development over the past decade is truly astounding. Modern browsers have evolved into powerful platforms capable of handling hardware-accelerated animations, native UI components like dialogs and popovers, flexible CSS layouts with Grid and Flexbox, and advanced JavaScript APIs for storage, notifications, geolocation, and even web assembly for high-performance computations. They've become so efficient at rendering complex web applications that many tasks once requiring heavy libraries can now be achieved with native features alone.
However, remnants of older practices — often referred to as "cruft" — still linger in many projects. For instance, Telebugs itself previously relied on the excellent Toastify.js library for displaying toast notifications. While Toastify.js served us well, browser advancements have rendered such dependencies less necessary for basic use cases.
Starting with Telebugs 1.9.0, we've transitioned to using the native Popover API for our toasts. This shift not only reduces our bundle size but also leverages built-in browser capabilities for better performance and accessibility.
In this post, we'll explore how this works and guide you through implementing a similar system in your own projects. Just a word of precaution: Toastify.js offers extensive configurability for advanced scenarios, whereas this native approach is tailored for simple, straightforward toasts. If you need features like custom positions, durations, or stacking, you might still prefer a dedicated library.
Live Demo
Try clicking these buttons to see the toasts in action right here:
Setting Up the CSS
To get started, we'll define the styles for our toasts in a file like
toasts.css. The key here is to style the toast element as
a fixed-position popover that slides in from the top with a smooth
animation. We'll explain each important CSS property as we go, so you
can understand and customize it easily.
First, the base .toast class sets up the visual
appearance and positioning:
.toast {
padding: 8px 12px;
font-weight: 500;
border-radius: 0.375rem;
text-decoration: none;
text-align: center;
z-index: 999999;
border: none;
margin: 0;
/* Creates a subtle shadow for depth, making the toast feel elevated */
box-shadow: 0 3px 6px -1px rgba(0, 0, 0, 0.15);
/* Positions the toast absolutely on the screen, independent of scrolling */
position: fixed;
/* Places it near the top, with some margin from the edge */
top: 40px;
/* Centers it horizontally */
left: 50%;
/* Shifts it left by half its width for perfect centering */
transform: translateX(-50%);
/* Applies the entrance animation with an ease-out curve for smoothness */
animation: slideDown 0.4s cubic-bezier(0.215, 0.61, 0.355, 1);
}
Next, we use the :popover-open pseudo-class to control
visibility. By default, popovers are hidden, so we set them to display
as inline-block when open:
.toast:popover-open {
display: inline-block;
}
The entrance animation is defined with keyframes. It slides the toast down from above while fading in:
@keyframes slideDown {
from {
/* Starts off-screen above, centered horizontally */
transform: translateX(-50%) translateY(-165px);
/* Fully transparent at the start */
opacity: 0;
}
to {
/* Ends in its final position */
transform: translateX(-50%) translateY(0);
/* Fully visible */
opacity: 1;
}
}
For different toast types, we use classes like
.toast--notice and .toast--alert to apply
colors. These rely on CSS variables for easy theming:
.toast--notice {
color: var(--color-emerald-600);
background-color: var(--color-emerald-50);
border: 1px solid var(--color-emerald-100);
}
.toast--alert {
color: var(--color-rose-600);
background-color: var(--color-rose-50);
border: 1px solid var(--color-rose-100);
}
To handle small screens, add a media query to limit the toast's width:
@media only screen and (max-width: 360px) {
.toast {
/* Ensures it fits on narrow devices with some padding */
max-width: calc(100% - 30px);
}
}
Finally, for the exit animation, we define a fade-out with a slight
scale down, and apply it via a .hiding class:
@keyframes fadeOut {
/* Starts at full size and opacity */
from {
opacity: 1;
transform: translateX(-50%) scale(1);
}
/* Fades out while slightly shrinking for a natural feel */
to {
opacity: 0;
transform: translateX(-50%) scale(0.95);
}
}
.toast.hiding {
animation: fadeOut 0.4s cubic-bezier(0.215, 0.61, 0.355, 1); /* Applies the exit animation */
}
Include this CSS in your project by linking it in your HTML head:
<link rel="stylesheet" href="toasts.css">.
If you're using CSS variables, define them in your root styles, e.g.,
:root {
--color-emerald-600: #059669; /* etc. */
}
Refer to Tailwind CSS documentation for color values if you're inspired by that palette.
HTML Setup
The HTML for the toast is simple. Use a <div> with
the popover="manual" attribute to enable the Popover API.
Add your toast classes and any framework-specific attributes if needed
(like data-turbo-temporary for
Hotwire/Turbo
or data-controller for Stimulus).
Here's an example in a Rails ERB template, but you can adapt it to plain HTML or other frameworks:
<% if flash[:alert] %>
<div popover="manual" class="toast toast--alert" data-turbo-temporary data-controller="toast">
<%= flash[:alert] %>
</div>
<% end %>
<% if flash[:notice] %>
<div popover="manual" class="toast toast--notice" data-turbo-temporary data-controller="toast">
<%= flash[:notice] %>
</div>
<% end %>
In plain HTML, you might dynamically create these elements with JavaScript when needed. The popover="manual" tells the browser to manage show/hide via JS methods like showPopover() and hidePopover().
JavaScript Controller
To handle showing and hiding, we use a Stimulus controller (part of Hotwire). This connects automatically when the element loads, shows the popover, and schedules a hide after 5 seconds.
import { Controller } from "@hotwired/stimulus";
export default class extends Controller {
connect() {
// Displays the popover immediately
this.element.showPopover();
setTimeout(() => {
// Schedules hide after 5000ms (5 seconds)
this.hide();
}, 5000);
}
hide() {
// Triggers the fade-out animation
this.element.classList.add("hiding");
setTimeout(() => {
this.element.hidePopover();
// Removes from DOM for cleanup
this.element.remove();
}, 400); // Matches the animation duration
}
}
If you're not using Stimulus, you can achieve the same with plain JavaScript by selecting the element and calling the methods directly. Ensure your project includes Hotwire if using this exact code — see the Stimulus documentation for setup.
Interactive Demo
To see this in action, here's a self-contained HTML demo. Copy this into a file (e.g., demo.html), open it in a modern browser (Chrome 114+, Firefox 125+, Safari 17+ for Popover API support), and click the button to trigger a toast. Note: For full compatibility, check Can I Use.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Native Toast Demo</title>
<style>
:root {
--color-emerald-600: #059669;
--color-emerald-50: #ecfdf5;
--color-emerald-100: #d1fae5;
--color-rose-600: #e11d48;
--color-rose-50: #fff1f2;
--color-rose-100: #ffe4e6;
}
.toast {
padding: 8px 12px;
font-weight: 500;
box-shadow: 0 3px 6px -1px rgba(0, 0, 0, 0.15);
position: fixed;
top: 40px;
left: 50%;
transform: translateX(-50%);
border-radius: 0.375rem;
text-decoration: none;
text-align: center;
z-index: 2147483647;
border: none;
margin: 0;
animation: slideDown 0.4s cubic-bezier(0.215, 0.61, 0.355, 1);
}
.toast:popover-open {
display: inline-block;
}
@keyframes slideDown {
from {
transform: translateX(-50%) translateY(-165px);
opacity: 0;
}
to {
transform: translateX(-50%) translateY(0);
opacity: 1;
}
}
.toast--notice {
color: var(--color-emerald-600);
background-color: var(--color-emerald-50);
border: 1px solid var(--color-emerald-100);
}
.toast--alert {
color: var(--color-rose-600);
background-color: var(--color-rose-50);
border: 1px solid var(--color-rose-100);
}
@media only screen and (max-width: 360px) {
.toast {
max-width: calc(100% - 30px);
}
}
@keyframes fadeOut {
from {
opacity: 1;
transform: translateX(-50%) scale(1);
}
to {
opacity: 0;
transform: translateX(-50%) scale(0.95);
}
}
.toast.hiding {
animation: fadeOut 0.4s cubic-bezier(0.215, 0.61, 0.355, 1);
}
</style>
</head>
<body>
<button onclick="showNoticeToast()">Show Notice Toast</button>
<button onclick="showAlertToast()">Show Alert Toast</button>
<script>
function createToast(type, message) {
const toast = document.createElement('div');
toast.setAttribute('popover', 'manual');
toast.classList.add('toast', `toast--${type}`);
toast.textContent = message;
document.body.appendChild(toast);
toast.showPopover();
setTimeout(() => {
toast.classList.add('hiding');
setTimeout(() => {
toast.hidePopover();
toast.remove();
}, 400);
}, 5000);
}
function showNoticeToast() {
createToast('notice', 'This is a success notice!');
}
function showAlertToast() {
createToast('alert', 'This is an error alert!');
}
</script>
</body>
</html>
This demo uses plain JavaScript to create and manage toasts dynamically. Click the buttons to see the slide-in and fade-out animations in action.
Conclusion
By switching to the native Popover API for toasts in Telebugs 1.9.0, we've simplified our codebase, improved performance, and embraced modern web standards. This approach is lightweight and easy to implement for basic notifications, reducing reliance on third-party libraries. If your project needs simple toasts, give this a try — it leverages what browsers already do best.
These improvements make Telebugs an even more polished tool for tracking and resolving errors in your applications. Ready to track errors? Learn more about Telebugs, check out the full changelog for 1.9.0, or explore our docs to get started today.
— Kyrylo