UI Components Easy
Snackbar / Bottom Toast
Material-style snackbar that slides up from the bottom with optional action button, auto-dismiss, and queue support. No libraries.
Open in Lab
MCP
vanilla-js css
Targets: JS HTML
Code
*,
*::before,
*::after {
box-sizing: border-box;
margin: 0;
padding: 0;
}
body {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
background: #f9fafb;
color: #111;
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
}
.demo {
background: #fff;
border-radius: 20px;
padding: 40px;
border: 1px solid #e5e7eb;
width: 400px;
text-align: center;
}
.demo-title {
font-size: 20px;
font-weight: 800;
margin-bottom: 8px;
}
.demo-desc {
font-size: 14px;
color: #6b7280;
line-height: 1.6;
margin-bottom: 28px;
}
.btn-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 10px;
}
.trigger-btn {
padding: 10px 16px;
background: #111827;
color: #fff;
border: none;
border-radius: 10px;
font-size: 14px;
font-weight: 600;
cursor: pointer;
transition: opacity 0.15s;
}
.trigger-btn:hover {
opacity: 0.85;
}
.trigger-btn--ghost {
grid-column: 1 / -1;
background: #f3f4f6;
color: #374151;
}
/* Snackbar container */
.snackbar-container {
position: fixed;
bottom: 24px;
left: 50%;
transform: translateX(-50%);
display: flex;
flex-direction: column;
gap: 8px;
align-items: center;
z-index: 9999;
pointer-events: none;
}
/* Individual snackbar */
.snackbar {
display: flex;
align-items: center;
gap: 12px;
background: #1f2937;
color: #fff;
padding: 12px 16px 12px 20px;
border-radius: 12px;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.2);
font-size: 14px;
font-weight: 500;
min-width: 280px;
max-width: 420px;
pointer-events: all;
animation: snack-in 0.3s cubic-bezier(0.34, 1.56, 0.64, 1) forwards;
}
.snackbar.snackbar--error {
background: #7f1d1d;
border-left: 3px solid #ef4444;
}
.snackbar.snackbar--out {
animation: snack-out 0.25s ease forwards;
}
@keyframes snack-in {
from {
opacity: 0;
transform: translateY(20px) scale(0.95);
}
to {
opacity: 1;
transform: translateY(0) scale(1);
}
}
@keyframes snack-out {
from {
opacity: 1;
transform: translateY(0) scale(1);
}
to {
opacity: 0;
transform: translateY(12px) scale(0.95);
}
}
.snackbar-msg {
flex: 1;
}
.snackbar-action {
background: none;
border: none;
color: #a5b4fc;
font-size: 13px;
font-weight: 700;
cursor: pointer;
padding: 4px 8px;
border-radius: 6px;
white-space: nowrap;
transition: background 0.15s;
text-transform: uppercase;
letter-spacing: 0.04em;
}
.snackbar-action:hover {
background: rgba(255, 255, 255, 0.1);
}
.snackbar-close {
background: none;
border: none;
color: rgba(255, 255, 255, 0.5);
cursor: pointer;
padding: 4px;
border-radius: 4px;
line-height: 1;
font-size: 16px;
transition: color 0.15s;
}
.snackbar-close:hover {
color: #fff;
}const container = document.getElementById("snackbarContainer");
const DURATION = 4000;
function showSnackbar(message, actionLabel = "", type = "") {
const el = document.createElement("div");
el.className = "snackbar" + (type ? ` snackbar--${type}` : "");
el.innerHTML = `
<span class="snackbar-msg">${message}</span>
${actionLabel ? `<button class="snackbar-action">${actionLabel}</button>` : ""}
<button class="snackbar-close" aria-label="Dismiss">✕</button>
`;
container.appendChild(el);
const dismiss = () => {
clearTimeout(timer);
el.classList.add("snackbar--out");
el.addEventListener("animationend", () => el.remove(), { once: true });
};
const timer = setTimeout(dismiss, DURATION);
el.querySelector(".snackbar-close").addEventListener("click", dismiss);
const actionBtn = el.querySelector(".snackbar-action");
if (actionBtn) {
actionBtn.addEventListener("click", () => {
// Action callback — customize per use case
dismiss();
});
}
}
// Trigger buttons
document.querySelectorAll(".trigger-btn[data-msg]").forEach((btn) => {
btn.addEventListener("click", () => {
showSnackbar(btn.dataset.msg, btn.dataset.action, btn.dataset.type);
});
});
// Spam test
document.getElementById("spamBtn").addEventListener("click", () => {
const messages = [
"Item added to cart",
"Notification sent",
"Settings saved",
"Image uploaded",
"Link copied",
];
messages.forEach((msg, i) => {
setTimeout(() => showSnackbar(msg), i * 300);
});
});<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Snackbar</title>
<link rel="stylesheet" href="style.css" />
</head>
<body>
<div class="demo">
<h1 class="demo-title">Snackbar</h1>
<p class="demo-desc">Slides up from the bottom. Supports an action button, auto-dismisses, and queues multiple messages.</p>
<div class="btn-grid">
<button class="trigger-btn" data-msg="Changes saved successfully" data-action="">Simple</button>
<button class="trigger-btn" data-msg="File deleted" data-action="Undo">With Undo</button>
<button class="trigger-btn" data-msg="Link copied to clipboard" data-action="">Copy confirm</button>
<button class="trigger-btn" data-msg="Profile updated" data-action="View">With action</button>
<button class="trigger-btn" data-msg="Network error — please try again" data-action="Retry" data-type="error">Error</button>
<button class="trigger-btn trigger-btn--ghost" id="spamBtn">Spam 5 messages</button>
</div>
</div>
<!-- Snackbar container -->
<div class="snackbar-container" id="snackbarContainer" aria-live="polite"></div>
<script src="script.js"></script>
</body>
</html>Material-style snackbar that slides up from the screen bottom, supports an optional action button (e.g. Undo), auto-dismisses after a timeout, and queues multiple messages. Pure vanilla JS.