UI Components Easy
ARIA Live Regions
Patterns for ARIA live regions demonstrating polite, assertive and status announcements for dynamic content updates.
Open in Lab
MCP
vanilla-js
Targets: JS HTML
Code
*,
*::before,
*::after {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: "Inter", system-ui, -apple-system, sans-serif;
background: #0a0a0a;
color: #e4e4e7;
line-height: 1.6;
min-height: 100vh;
}
.demo {
max-width: 960px;
margin: 0 auto;
padding: 3rem 1.5rem;
}
.demo-title {
font-size: 1.75rem;
font-weight: 700;
color: #fafafa;
letter-spacing: -0.02em;
}
.demo-sub {
color: #a1a1aa;
margin-top: 0.25rem;
font-size: 0.95rem;
}
/* Region Cards */
.regions {
display: grid;
gap: 1.5rem;
margin-top: 2rem;
}
.region-card {
background: #111113;
border: 1px solid #27272a;
border-radius: 12px;
padding: 1.5rem;
}
.region-header {
display: flex;
align-items: center;
gap: 0.75rem;
margin-bottom: 0.5rem;
}
.region-title {
font-size: 1.1rem;
font-weight: 600;
color: #fafafa;
}
.region-badge {
font-size: 0.7rem;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.08em;
padding: 0.2rem 0.6rem;
border-radius: 100px;
}
.region-badge--polite {
background: rgba(34, 197, 94, 0.15);
color: #4ade80;
border: 1px solid rgba(34, 197, 94, 0.3);
}
.region-badge--assertive {
background: rgba(239, 68, 68, 0.15);
color: #f87171;
border: 1px solid rgba(239, 68, 68, 0.3);
}
.region-badge--status {
background: rgba(59, 130, 246, 0.15);
color: #60a5fa;
border: 1px solid rgba(59, 130, 246, 0.3);
}
.region-desc {
font-size: 0.875rem;
color: #a1a1aa;
margin-bottom: 1rem;
}
.region-desc code {
background: #1e1e22;
padding: 0.15rem 0.4rem;
border-radius: 4px;
font-size: 0.8rem;
color: #d4d4d8;
font-family: "SF Mono", "Fira Code", monospace;
}
.region-output {
background: #09090b;
border: 1px solid #1e1e22;
border-radius: 8px;
padding: 1rem;
min-height: 60px;
margin-bottom: 1rem;
}
.region-actions {
display: flex;
gap: 0.5rem;
}
/* Notification items */
.notification-item {
display: flex;
align-items: flex-start;
gap: 0.75rem;
padding: 0.5rem 0;
animation: slideIn 0.3s ease-out;
}
.notification-item + .notification-item {
border-top: 1px solid #1e1e22;
}
.notification-dot {
width: 8px;
height: 8px;
border-radius: 50%;
background: #4ade80;
margin-top: 0.45rem;
flex-shrink: 0;
}
.notification-text {
font-size: 0.875rem;
color: #d4d4d8;
}
.notification-time {
font-size: 0.75rem;
color: #52525b;
margin-top: 0.15rem;
}
/* Error alert */
.error-alert {
display: flex;
align-items: center;
gap: 0.75rem;
padding: 0.75rem;
background: rgba(239, 68, 68, 0.1);
border: 1px solid rgba(239, 68, 68, 0.2);
border-radius: 8px;
animation: shakeIn 0.4s ease-out;
}
.error-icon {
width: 20px;
height: 20px;
color: #f87171;
flex-shrink: 0;
}
.error-text {
font-size: 0.875rem;
color: #fca5a5;
}
/* Status / Search */
.search-demo {
margin-bottom: 0.75rem;
}
.search-label {
display: block;
font-size: 0.8rem;
color: #71717a;
margin-bottom: 0.35rem;
}
.search-input {
width: 100%;
padding: 0.6rem 0.85rem;
background: #09090b;
border: 1px solid #27272a;
border-radius: 8px;
color: #e4e4e7;
font-size: 0.875rem;
font-family: inherit;
outline: none;
transition: border-color 0.2s;
}
.search-input:focus {
border-color: #3b82f6;
box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.15);
}
.status-text {
font-size: 0.875rem;
color: #60a5fa;
}
/* Buttons */
.btn {
display: inline-flex;
align-items: center;
gap: 0.4rem;
padding: 0.55rem 1rem;
font-size: 0.825rem;
font-weight: 500;
font-family: inherit;
border: 1px solid #27272a;
border-radius: 8px;
background: #18181b;
color: #e4e4e7;
cursor: pointer;
transition: all 0.15s;
}
.btn:hover {
background: #27272a;
border-color: #3f3f46;
}
.btn:focus-visible {
outline: 2px solid #3b82f6;
outline-offset: 2px;
}
.btn--ghost {
background: transparent;
border-color: transparent;
color: #a1a1aa;
}
.btn--ghost:hover {
background: #1e1e22;
color: #e4e4e7;
}
.btn--danger {
background: rgba(239, 68, 68, 0.15);
border-color: rgba(239, 68, 68, 0.3);
color: #f87171;
}
.btn--danger:hover {
background: rgba(239, 68, 68, 0.25);
}
.btn--sm {
padding: 0.35rem 0.7rem;
font-size: 0.75rem;
}
/* Announcement Log */
.log-section {
margin-top: 2rem;
background: #111113;
border: 1px solid #27272a;
border-radius: 12px;
padding: 1.5rem;
}
.log-header {
display: flex;
align-items: center;
gap: 1rem;
flex-wrap: wrap;
margin-bottom: 1rem;
}
.log-title {
font-size: 1rem;
font-weight: 600;
color: #fafafa;
}
.log-desc {
font-size: 0.8rem;
color: #71717a;
flex: 1;
}
.log-entries {
background: #09090b;
border: 1px solid #1e1e22;
border-radius: 8px;
padding: 0.75rem;
max-height: 260px;
overflow-y: auto;
font-family: "SF Mono", "Fira Code", monospace;
font-size: 0.8rem;
}
.log-entry {
display: flex;
gap: 0.75rem;
padding: 0.4rem 0;
animation: slideIn 0.2s ease-out;
}
.log-entry + .log-entry {
border-top: 1px solid #1e1e22;
}
.log-tag {
font-size: 0.65rem;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.06em;
padding: 0.1rem 0.4rem;
border-radius: 4px;
flex-shrink: 0;
height: fit-content;
margin-top: 0.15rem;
}
.log-tag--polite {
background: rgba(34, 197, 94, 0.15);
color: #4ade80;
}
.log-tag--assertive {
background: rgba(239, 68, 68, 0.15);
color: #f87171;
}
.log-tag--status {
background: rgba(59, 130, 246, 0.15);
color: #60a5fa;
}
.log-message {
color: #a1a1aa;
}
.log-empty {
color: #52525b;
text-align: center;
padding: 1rem;
font-family: inherit;
}
/* Animations */
@keyframes slideIn {
from {
opacity: 0;
transform: translateY(-6px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
@keyframes shakeIn {
0% {
opacity: 0;
transform: translateX(-8px);
}
40% {
transform: translateX(4px);
}
70% {
transform: translateX(-2px);
}
100% {
opacity: 1;
transform: translateX(0);
}
}
/* Scrollbar */
.log-entries::-webkit-scrollbar {
width: 4px;
}
.log-entries::-webkit-scrollbar-track {
background: transparent;
}
.log-entries::-webkit-scrollbar-thumb {
background: #27272a;
border-radius: 4px;
}(() => {
const politeRegion = document.getElementById("polite-region");
const assertiveRegion = document.getElementById("assertive-region");
const statusRegion = document.getElementById("status-region");
const logEntries = document.getElementById("log-entries");
const notifications = [
"Alice commented on your pull request.",
"Deployment to production completed successfully.",
"New team member Bob joined the project.",
"Your report export is ready for download.",
"Sprint review meeting starts in 15 minutes.",
"3 new commits pushed to the main branch.",
"Security scan passed with no vulnerabilities.",
"Your subscription has been renewed.",
];
const errors = [
"Payment failed: card declined. Update your billing information.",
"Session expired. You will be signed out in 30 seconds.",
"Critical: database connection lost. Retrying...",
"API rate limit exceeded. Please wait before retrying.",
];
const items = [
"Dashboard",
"Analytics",
"Reports",
"Settings",
"Users",
"Billing",
"Integrations",
"Notifications",
"Security",
"Team",
"Projects",
"API Keys",
];
let notifIndex = 0;
let errorIndex = 0;
function addToLog(type, message) {
const emptyMsg = logEntries.querySelector(".log-empty");
if (emptyMsg) emptyMsg.remove();
const entry = document.createElement("div");
entry.className = "log-entry";
entry.innerHTML = `
<span class="log-tag log-tag--${type}">${type}</span>
<span class="log-message">${message}</span>
`;
logEntries.prepend(entry);
}
// Polite: Add Notification
document.getElementById("add-notification").addEventListener("click", () => {
const msg = notifications[notifIndex % notifications.length];
notifIndex++;
const item = document.createElement("div");
item.className = "notification-item";
const now = new Date();
const time = now.toLocaleTimeString([], {
hour: "2-digit",
minute: "2-digit",
second: "2-digit",
});
item.innerHTML = `
<span class="notification-dot" aria-hidden="true"></span>
<div>
<div class="notification-text">${msg}</div>
<div class="notification-time">${time}</div>
</div>
`;
politeRegion.prepend(item);
addToLog("polite", msg);
});
// Clear polite
document.getElementById("clear-polite").addEventListener("click", () => {
politeRegion.innerHTML = "";
});
// Assertive: Trigger Error
document.getElementById("trigger-error").addEventListener("click", () => {
const msg = errors[errorIndex % errors.length];
errorIndex++;
assertiveRegion.innerHTML = `
<div class="error-alert">
<svg class="error-icon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none"
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<circle cx="12" cy="12" r="10"/>
<line x1="15" y1="9" x2="9" y2="15"/>
<line x1="9" y1="9" x2="15" y2="15"/>
</svg>
<span class="error-text">${msg}</span>
</div>
`;
addToLog("assertive", msg);
});
// Clear assertive
document.getElementById("clear-assertive").addEventListener("click", () => {
assertiveRegion.innerHTML = "";
});
// Status: Search filter
document.getElementById("search-input").addEventListener("input", (e) => {
const query = e.target.value.toLowerCase().trim();
const filtered = query ? items.filter((i) => i.toLowerCase().includes(query)) : items;
const count = filtered.length;
const msg = count === 1 ? "1 result found" : `${count} results found`;
statusRegion.innerHTML = `<span class="status-text">${msg}</span>`;
addToLog("status", msg);
});
// Clear log
document.getElementById("clear-log").addEventListener("click", () => {
logEntries.innerHTML =
'<div class="log-empty">No announcements yet. Trigger an action above.</div>';
});
})();<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>ARIA Live Regions</title>
<link rel="stylesheet" href="style.css" />
</head>
<body>
<div class="demo">
<h1 class="demo-title">ARIA Live Regions</h1>
<p class="demo-sub">Polite, assertive, and status announcements for dynamic content updates.</p>
<div class="regions">
<!-- Polite Region -->
<section class="region-card">
<div class="region-header">
<span class="region-badge region-badge--polite">polite</span>
<h2 class="region-title">Notification Feed</h2>
</div>
<p class="region-desc">
<code>aria-live="polite"</code> waits for the user to finish their current task before announcing.
</p>
<div class="region-output" aria-live="polite" aria-relevant="additions" id="polite-region">
<!-- Notifications injected here -->
</div>
<div class="region-actions">
<button class="btn" id="add-notification">Add Notification</button>
<button class="btn btn--ghost" id="clear-polite">Clear</button>
</div>
</section>
<!-- Assertive Region -->
<section class="region-card">
<div class="region-header">
<span class="region-badge region-badge--assertive">assertive</span>
<h2 class="region-title">Error Alert</h2>
</div>
<p class="region-desc">
<code>aria-live="assertive"</code> interrupts immediately — use sparingly for critical errors.
</p>
<div class="region-output" aria-live="assertive" aria-atomic="true" id="assertive-region">
<!-- Error alerts injected here -->
</div>
<div class="region-actions">
<button class="btn btn--danger" id="trigger-error">Trigger Error</button>
<button class="btn btn--ghost" id="clear-assertive">Clear</button>
</div>
</section>
<!-- Status Role -->
<section class="region-card">
<div class="region-header">
<span class="region-badge region-badge--status">status</span>
<h2 class="region-title">Search Results Counter</h2>
</div>
<p class="region-desc">
<code>role="status"</code> is implicitly <code>aria-live="polite"</code> and announces state changes.
</p>
<div class="search-demo">
<label for="search-input" class="search-label">Search items:</label>
<input type="text" id="search-input" class="search-input" placeholder="Type to filter..." autocomplete="off" />
</div>
<div class="region-output" role="status" id="status-region">
<span class="status-text">12 results found</span>
</div>
</section>
</div>
<!-- Visual Announcement Log -->
<section class="log-section">
<div class="log-header">
<h2 class="log-title">Announcement Log</h2>
<p class="log-desc">Visual mirror of what screen readers would announce</p>
<button class="btn btn--ghost btn--sm" id="clear-log">Clear Log</button>
</div>
<div class="log-entries" id="log-entries" aria-hidden="true">
<div class="log-empty">No announcements yet. Trigger an action above.</div>
</div>
</section>
</div>
<script src="script.js"></script>
</body>
</html>Demonstrates the three main ARIA live region modes: aria-live="polite" for non-urgent updates, aria-live="assertive" for critical alerts, and role="status" for contextual state changes. Each section includes trigger buttons and a visual log mirroring what screen readers announce.