UI Components Easy
Screen Reader Announcer
ARIA live region announcer that broadcasts status messages to screen readers without visual disruption. Vanilla JS utility.
Open in Lab
MCP
vanilla-js
Targets: JS HTML
Code
*,
*::before,
*::after {
box-sizing: border-box;
margin: 0;
padding: 0;
}
/* Visually hidden but announced by screen readers */
.sr-only {
position: absolute;
width: 1px;
height: 1px;
padding: 0;
margin: -1px;
overflow: hidden;
clip: rect(0, 0, 0, 0);
white-space: nowrap;
border: 0;
}
body {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
background: #0d1117;
color: #e6edf3;
min-height: 100vh;
padding: 32px 16px;
display: flex;
justify-content: center;
}
.demo {
width: 100%;
max-width: 720px;
display: flex;
flex-direction: column;
gap: 24px;
}
.demo-title {
font-size: 20px;
font-weight: 800;
}
.demo-desc {
font-size: 14px;
color: #8b949e;
line-height: 1.65;
}
.demo-desc strong {
color: #cdd6f4;
}
.announce-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 16px;
}
@media (max-width: 560px) {
.announce-grid {
grid-template-columns: 1fr;
}
}
.announce-card {
background: #161b22;
border: 1px solid #21262d;
border-radius: 12px;
padding: 18px;
}
.announce-card--assertive {
border-color: rgba(248, 81, 73, 0.15);
}
.ac-label {
font-size: 11px;
font-weight: 800;
text-transform: uppercase;
letter-spacing: 0.07em;
color: #3fb950;
margin-bottom: 6px;
}
.ac-label--assertive {
color: #f85149;
}
.ac-desc {
font-size: 12px;
color: #6c7086;
margin-bottom: 14px;
line-height: 1.5;
}
.ac-examples {
display: flex;
flex-direction: column;
gap: 8px;
}
.ac-btn {
background: #21262d;
border: 1px solid #30363d;
color: #cdd6f4;
font-size: 13px;
font-weight: 500;
padding: 8px 14px;
border-radius: 8px;
cursor: pointer;
text-align: left;
transition: all 0.15s;
}
.ac-btn:hover {
background: #30363d;
border-color: #484f58;
}
.ac-btn:focus-visible {
outline: 2px solid #6366f1;
outline-offset: 2px;
}
.ac-btn--danger {
border-color: rgba(248, 81, 73, 0.2);
color: #fca5a5;
}
.ac-btn--danger:hover {
background: rgba(248, 81, 73, 0.08);
}
.ac-btn--warning {
border-color: rgba(210, 153, 34, 0.2);
color: #fcd34d;
}
.ac-btn--warning:hover {
background: rgba(210, 153, 34, 0.08);
}
/* Log */
.announce-log-wrap {
background: #161b22;
border: 1px solid #21262d;
border-radius: 12px;
overflow: hidden;
}
.announce-log-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 12px 16px;
border-bottom: 1px solid #21262d;
background: #1c2128;
}
.al-title {
font-size: 13px;
font-weight: 700;
color: #e6edf3;
}
.al-clear {
background: none;
border: none;
color: #6c7086;
font-size: 12px;
cursor: pointer;
transition: color 0.15s;
}
.al-clear:hover {
color: #e6edf3;
}
.announce-log {
padding: 12px;
min-height: 100px;
max-height: 200px;
overflow-y: auto;
display: flex;
flex-direction: column;
gap: 6px;
}
.al-empty {
font-size: 13px;
color: #4a555f;
padding: 8px;
}
.al-entry {
display: flex;
gap: 10px;
align-items: flex-start;
background: #1c2128;
border-radius: 8px;
padding: 8px 12px;
animation: al-in 0.2s ease;
font-size: 13px;
}
@keyframes al-in {
from {
opacity: 0;
transform: translateX(-6px);
}
to {
opacity: 1;
transform: translateX(0);
}
}
.al-badge {
font-size: 10px;
font-weight: 800;
padding: 2px 7px;
border-radius: 4px;
text-transform: uppercase;
letter-spacing: 0.05em;
flex-shrink: 0;
margin-top: 1px;
}
.al-badge--polite {
background: rgba(63, 185, 80, 0.15);
color: #3fb950;
}
.al-badge--assertive {
background: rgba(248, 81, 73, 0.15);
color: #f85149;
}
.al-msg {
color: #cdd6f4;
flex: 1;
line-height: 1.5;
}
.al-time {
font-size: 11px;
color: #4a555f;
flex-shrink: 0;
font-family: Menlo, monospace;
}const politeRegion = document.getElementById("livePolite");
const assertiveRegion = document.getElementById("liveAssertive");
const log = document.getElementById("announceLog");
function pad(n) {
return String(n).padStart(2, "0");
}
function getTime() {
const d = new Date();
return `${pad(d.getHours())}:${pad(d.getMinutes())}:${pad(d.getSeconds())}`;
}
function announce(msg, type) {
// Clear and re-set to force re-announcement
const region = type === "assertive" ? assertiveRegion : politeRegion;
region.textContent = "";
setTimeout(() => {
region.textContent = msg;
}, 50);
// Update visual log
const empty = log.querySelector(".al-empty");
if (empty) empty.remove();
const entry = document.createElement("div");
entry.className = "al-entry";
entry.innerHTML = `
<span class="al-badge al-badge--${type}">${type}</span>
<span class="al-msg">${msg}</span>
<span class="al-time">${getTime()}</span>
`;
log.appendChild(entry);
log.scrollTop = log.scrollHeight;
}
document.querySelectorAll(".ac-btn").forEach((btn) => {
btn.addEventListener("click", () => {
const region = btn.dataset.region;
const msg = btn.dataset.msg;
announce(msg, region);
});
});
document.getElementById("alClear").addEventListener("click", () => {
log.innerHTML = '<div class="al-empty">Press a button above to see announcements here.</div>';
politeRegion.textContent = "";
assertiveRegion.textContent = "";
});<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Screen Reader Announce</title>
<link rel="stylesheet" href="style.css" />
</head>
<body>
<!-- ARIA live regions — invisible but announced by screen readers -->
<div aria-live="polite" aria-atomic="true" class="sr-only" id="livePolite"></div>
<div aria-live="assertive" aria-atomic="true" class="sr-only" id="liveAssertive"></div>
<div class="demo">
<h2 class="demo-title">Screen Reader Announce</h2>
<p class="demo-desc">Live regions let screen readers announce dynamic content without moving focus. Use <strong>polite</strong> for updates and <strong>assertive</strong> for urgent alerts.</p>
<div class="announce-grid">
<div class="announce-card">
<div class="ac-label">Polite Region</div>
<div class="ac-desc">Waits for the user to finish reading before announcing.</div>
<div class="ac-examples">
<button class="ac-btn" data-region="polite" data-msg="Shopping cart updated. 3 items, total $47.99.">🛒 Cart updated</button>
<button class="ac-btn" data-region="polite" data-msg="Search results: 24 items found for "sneakers".">🔍 Search results</button>
<button class="ac-btn" data-region="polite" data-msg="File uploaded successfully. Design mockup v2.png is ready.">✅ Upload complete</button>
<button class="ac-btn" data-region="polite" data-msg="Form saved. Your changes have been saved automatically.">💾 Auto-save</button>
</div>
</div>
<div class="announce-card announce-card--assertive">
<div class="ac-label ac-label--assertive">Assertive Region</div>
<div class="ac-desc">Interrupts immediately. Use sparingly for critical alerts.</div>
<div class="ac-examples">
<button class="ac-btn ac-btn--danger" data-region="assertive" data-msg="Error: Payment failed. Please check your card details and try again.">❌ Payment error</button>
<button class="ac-btn ac-btn--warning" data-region="assertive" data-msg="Warning: Your session expires in 2 minutes. Save your work.">⚠ Session expiry</button>
<button class="ac-btn ac-btn--danger" data-region="assertive" data-msg="Form error: Email address is required.">🚨 Form error</button>
</div>
</div>
</div>
<div class="announce-log-wrap">
<div class="announce-log-header">
<span class="al-title">Announcement Log</span>
<button class="al-clear" id="alClear">Clear</button>
</div>
<div class="announce-log" id="announceLog">
<div class="al-empty">Press a button above to see announcements here.</div>
</div>
</div>
</div>
<script src="script.js"></script>
</body>
</html>ARIA live region utility that queues announcements for screen readers. Supports polite and assertive modes. The demo shows interactive buttons that trigger various announcements — visible in the log below.