17 / 20
All 20 in 20 CSS Empty States
- 01No Search Results State
- 02First-Time Onboarding State
- 03Empty Shopping Cart State
- 04Inbox Zero / All Caught Up State
- 05Empty Data Table / No Rows State
- 06Filtered List No Matches State
- 07Empty Dashboard / No Analytics Data
- 08500 Server Error Page Layout
- 09Empty Wishlist / Favorites State
- 10Empty Notification Center Flyout
- 11Failed to Load / Connection Error State
- 12Offline / No Internet Connection State
- 13Permission Denied / Restricted Access State
- 14404 Page Not Found Layout
- 15Deleted or Archived Items State
- 16Skeleton Loading to Empty State Transition
- 17Accessible Empty State (ARIA Live)
- 18Pure CSS Illustrated Empty State
- 19Dark Mode Compatible Empty State
- 20Inline / Micro Empty State Component
Accessible Empty State (ARIA Live)
The accessible version of a no-results state, in a library catalogue: a visually hidden role="status" region announces the outcome, focus moves to the results container so a screen reader lands on the message rather than hunting for it, and the recovery suggestions are real buttons that re-run the search. Nothing about the accessibility work is visible — which is the point.
Nucleo: 44,000+ SVG icons in five families · All Icons Bundle $149
Nucleo · Affiliate link
The code
<div class="est-17">
<a class="est-17__skip" href="#est-17-results">Skip to results</a>
<div class="est-17__shell">
<header class="est-17__top">
<span class="est-17__brand">Ordnance Library</span>
<nav class="est-17__nav" aria-label="Catalogue">
<a href="#est-17">Maps</a>
<a href="#est-17">Surveys</a>
<a href="#est-17">Photographs</a>
</nav>
</header>
<main class="est-17__main">
<form class="est-17__form" id="est-17-form" role="search">
<label class="est-17__label" for="est-17-q">Search the catalogue</label>
<div class="est-17__row">
<input class="est-17__input" type="search" id="est-17-q" name="q" value="tidal survey 1974" autocomplete="off"
aria-describedby="est-17-hint" aria-controls="est-17-results">
<button class="est-17__go" type="submit">Search</button>
</div>
<p class="est-17__hint" id="est-17-hint">Type at least two characters. Results update when you press Search.</p>
</form>
<div class="est-17__vh" role="status" aria-live="polite" aria-atomic="true" id="est-17-live"></div>
<section class="est-17__results" id="est-17-results" tabindex="-1" aria-labelledby="est-17-rh">
<h2 class="est-17__rh" id="est-17-rh">Results</h2>
<div class="est-17__empty" id="est-17-empty">
<span class="est-17__glyph" aria-hidden="true">
<svg viewBox="0 0 24 24" fill="none" focusable="false"><rect x="4" y="5" width="16" height="14" rx="2.2" stroke="currentColor" stroke-width="1.6"/><path d="M8 9.5h8M8 13h5" stroke="currentColor" stroke-width="1.6" stroke-linecap="round"/></svg>
</span>
<p class="est-17__eh">No catalogue entries for that phrase</p>
<p class="est-17__ep">Sheet numbers are indexed without spaces, and surveys before 1980 are filed by county rather than year.</p>
<ul class="est-17__tries">
<li><button class="est-17__try" type="button" data-q="tidal survey">tidal survey</button></li>
<li><button class="est-17__try" type="button" data-q="devon county 1974">devon county 1974</button></li>
<li><button class="est-17__try" type="button" data-q="sheet 118">sheet 118</button></li>
</ul>
</div>
<ol class="est-17__list" id="est-17-list" hidden></ol>
</section>
</main>
</div>
</div><div class="est-17">
<a class="est-17__skip" href="#est-17-results">Skip to results</a>
<div class="est-17__shell">
<header class="est-17__top">
<span class="est-17__brand">Ordnance Library</span>
<nav class="est-17__nav" aria-label="Catalogue">
<a href="#est-17">Maps</a>
<a href="#est-17">Surveys</a>
<a href="#est-17">Photographs</a>
</nav>
</header>
<main class="est-17__main">
<form class="est-17__form" id="est-17-form" role="search">
<label class="est-17__label" for="est-17-q">Search the catalogue</label>
<div class="est-17__row">
<input class="est-17__input" type="search" id="est-17-q" name="q" value="tidal survey 1974" autocomplete="off"
aria-describedby="est-17-hint" aria-controls="est-17-results">
<button class="est-17__go" type="submit">Search</button>
</div>
<p class="est-17__hint" id="est-17-hint">Type at least two characters. Results update when you press Search.</p>
</form>
<div class="est-17__vh" role="status" aria-live="polite" aria-atomic="true" id="est-17-live"></div>
<section class="est-17__results" id="est-17-results" tabindex="-1" aria-labelledby="est-17-rh">
<h2 class="est-17__rh" id="est-17-rh">Results</h2>
<div class="est-17__empty" id="est-17-empty">
<span class="est-17__glyph" aria-hidden="true">
<svg viewBox="0 0 24 24" fill="none" focusable="false"><rect x="4" y="5" width="16" height="14" rx="2.2" stroke="currentColor" stroke-width="1.6"/><path d="M8 9.5h8M8 13h5" stroke="currentColor" stroke-width="1.6" stroke-linecap="round"/></svg>
</span>
<p class="est-17__eh">No catalogue entries for that phrase</p>
<p class="est-17__ep">Sheet numbers are indexed without spaces, and surveys before 1980 are filed by county rather than year.</p>
<ul class="est-17__tries">
<li><button class="est-17__try" type="button" data-q="tidal survey">tidal survey</button></li>
<li><button class="est-17__try" type="button" data-q="devon county 1974">devon county 1974</button></li>
<li><button class="est-17__try" type="button" data-q="sheet 118">sheet 118</button></li>
</ul>
</div>
<ol class="est-17__list" id="est-17-list" hidden></ol>
</section>
</main>
</div>
</div>@import url('https://fonts.googleapis.com/css2?family=Geist:wght@400;500;600&family=Geist+Mono:wght@400;500&display=swap');
.est-17 {
--bg: #f7f7f4;
--surface: #ffffff;
--sunk: #eff0eb;
--line: #e4e5df;
--line-2: #d1d2ca;
--ink: #191a17;
--muted: #585a53;
--faint: #878980;
--accent: #1f5c4d;
--focus: #0b62d6;
position: relative;
width: 100%;
min-height: 100vh;
min-height: 100svh;
display: grid;
box-sizing: border-box;
background: var(--bg);
color: var(--ink);
color-scheme: light;
font-synthesis: none;
font-family: 'Geist',ui-sans-serif,system-ui,-apple-system,sans-serif;
-webkit-font-smoothing: antialiased;
}
.est-17 *,
.est-17 *::before,
.est-17 *::after {
box-sizing: border-box;
}
.est-17 [hidden] {
display: none !important;
}
.est-17__vh {
position: absolute;
inline-size: 1px;
block-size: 1px;
overflow: hidden;
clip-path: inset(50%);
white-space: nowrap;
}
@supports (color: oklch(0.7 0.1 300)) {
.est-17 {
--bg: oklch(0.974 0.005 110);
--surface: oklch(1 0 0);
--sunk: oklch(0.955 0.006 110);
--line: oklch(0.918 0.006 110);
--line-2: oklch(0.858 0.008 110);
--ink: oklch(0.19 0.008 120);
--muted: oklch(0.465 0.01 120);
--faint: oklch(0.615 0.011 120);
--accent: oklch(0.4 0.075 168);
--focus: oklch(0.52 0.19 258);
}
}
.est-17__skip {
position: absolute;
inset-block-start: .5rem;
inset-inline-start: .5rem;
z-index: 9;
translate: 0 -160%;
padding: .6rem .9rem;
font-size: .85rem;
font-weight: 500;
color: #fff;
background: var(--accent);
border-radius: 8px;
text-decoration: none;
transition: translate .18s ease;
}
.est-17__skip:focus-visible {
translate: 0 0;
outline: 3px solid var(--focus);
outline-offset: 2px;
}
.est-17__shell {
inline-size: min(60rem,100%);
justify-self: center;
display: grid;
grid-template-rows: auto minmax(0,1fr);
align-content: start;
}
.est-17__top {
display: flex;
flex-wrap: wrap;
gap: .75rem 1.75rem;
align-items: baseline;
padding: 1.15rem clamp(1rem,3vw,2rem);
border-block-end: 1px solid var(--line);
}
.est-17__brand {
font-size: 1rem;
font-weight: 600;
letter-spacing: -.025em;
}
.est-17__nav {
display: flex;
gap: 1.15rem;
}
.est-17__nav a {
font-size: .85rem;
color: var(--muted);
text-decoration: none;
border-block-end: 1px solid transparent;
transition: color .16s, border-color .16s;
}
.est-17__nav a:hover {
color: var(--ink);
border-color: var(--accent);
}
.est-17__main {
padding: clamp(1.5rem,4vw,3rem) clamp(1rem,3vw,2rem);
display: grid;
gap: 1.5rem;
align-content: start;
}
.est-17__form {
display: grid;
gap: .5rem;
max-inline-size: 34rem;
}
.est-17__label {
font-size: .9rem;
font-weight: 600;
letter-spacing: -.015em;
}
.est-17__row {
display: flex;
flex-wrap: wrap;
gap: .5rem;
}
.est-17__input {
flex: 1 1 15rem;
min-inline-size: 0;
min-block-size: 2.9rem;
padding: .7rem .9rem;
font: inherit;
font-size: .92rem;
color: var(--ink);
background: var(--surface);
border: 1px solid var(--line-2);
border-radius: 10px;
outline: none;
}
.est-17__input:focus-visible {
border-color: var(--focus);
box-shadow: 0 0 0 3px color-mix(in oklab, var(--focus) 22%, transparent);
}
.est-17__go {
min-block-size: 2.9rem;
padding: .7rem 1.15rem;
font: inherit;
font-size: .88rem;
font-weight: 500;
cursor: pointer;
color: #fff;
background: var(--accent);
border: 1px solid transparent;
border-radius: 10px;
transition: translate .16s, background .16s;
}
.est-17__go:hover {
translate: 0 -1px;
background: color-mix(in oklab, var(--accent) 88%, #000);
}
.est-17__hint {
margin: 0;
font-size: .8rem;
color: var(--faint);
}
.est-17__results {
display: grid;
gap: 1rem;
}
.est-17__results:focus-visible {
outline: 3px solid var(--focus);
outline-offset: 6px;
border-radius: 12px;
}
.est-17__rh {
margin: 0;
font-family: 'Geist Mono',ui-monospace,monospace;
font-size: .7rem;
font-weight: 500;
letter-spacing: .12em;
text-transform: uppercase;
color: var(--faint);
}
.est-17__empty {
display: grid;
justify-items: center;
text-align: center;
gap: .5rem;
padding: clamp(2rem,6vh,3.25rem) 1.5rem;
background: var(--surface);
border: 1px solid var(--line);
border-radius: 18px;
box-shadow: 0 1px 2px rgba(25,26,23,.04);
animation: est-17-in .45s cubic-bezier(.2,.7,.3,1) both;
}
@keyframes est-17-in {
from {
opacity: 0;
translate: 0 6px;
}
to {
opacity: 1;
translate: 0 0;
}
}
.est-17__glyph {
inline-size: 3.4rem;
block-size: 3.4rem;
display: grid;
place-items: center;
margin-block-end: .4rem;
color: var(--accent);
background: color-mix(in oklab, var(--accent) 8%, var(--surface));
border-radius: 14px;
box-shadow: 0 0 0 1px color-mix(in oklab, var(--accent) 18%, transparent);
}
.est-17__glyph svg {
inline-size: 1.45rem;
block-size: 1.45rem;
}
.est-17__eh {
margin: 0;
font-size: 1.08rem;
font-weight: 600;
letter-spacing: -.022em;
text-wrap: balance;
}
.est-17__ep {
margin: 0;
max-inline-size: 48ch;
font-size: .89rem;
line-height: 1.6;
color: var(--muted);
text-wrap: pretty;
}
.est-17__tries {
margin: .9rem 0 0;
padding: 0;
list-style: none;
display: flex;
flex-wrap: wrap;
gap: .5rem;
justify-content: center;
}
.est-17__try {
min-block-size: 2.6rem;
padding: .6rem .9rem;
font: inherit;
font-family: 'Geist Mono',ui-monospace,monospace;
font-size: .8rem;
cursor: pointer;
color: var(--ink);
background: var(--sunk);
border: 1px solid var(--line-2);
border-radius: 9px;
transition: background .16s, border-color .16s;
}
.est-17__try:hover {
background: color-mix(in oklab, var(--accent) 10%, var(--sunk));
border-color: var(--accent);
}
.est-17__try:focus-visible,
.est-17__go:focus-visible,
.est-17__nav a:focus-visible {
outline: 3px solid var(--focus);
outline-offset: 2px;
}
.est-17__list {
margin: 0;
padding: 0;
list-style: none;
display: grid;
gap: .5rem;
}
.est-17__item {
display: grid;
gap: .25rem;
padding: .9rem 1.05rem;
background: var(--surface);
border: 1px solid var(--line);
border-radius: 13px;
animation: est-17-in .4s cubic-bezier(.2,.7,.3,1) both;
}
.est-17__it {
font-size: .93rem;
font-weight: 500;
letter-spacing: -.015em;
}
.est-17__im {
font-family: 'Geist Mono',ui-monospace,monospace;
font-size: .74rem;
color: var(--faint);
}
@media (prefers-reduced-motion: reduce) {
.est-17 *,
.est-17 *::before,
.est-17 *::after {
animation: none !important;
transition: none !important;
}
}
@media (forced-colors: active) {
.est-17__empty,
.est-17__item,
.est-17__go,
.est-17__try {
border-color: CanvasText;
}
}@import url('https://fonts.googleapis.com/css2?family=Geist:wght@400;500;600&family=Geist+Mono:wght@400;500&display=swap');
.est-17 {
--bg: #f7f7f4;
--surface: #ffffff;
--sunk: #eff0eb;
--line: #e4e5df;
--line-2: #d1d2ca;
--ink: #191a17;
--muted: #585a53;
--faint: #878980;
--accent: #1f5c4d;
--focus: #0b62d6;
position: relative;
width: 100%;
min-height: 100vh;
min-height: 100svh;
display: grid;
box-sizing: border-box;
background: var(--bg);
color: var(--ink);
color-scheme: light;
font-synthesis: none;
font-family: 'Geist',ui-sans-serif,system-ui,-apple-system,sans-serif;
-webkit-font-smoothing: antialiased;
}
.est-17 *,
.est-17 *::before,
.est-17 *::after {
box-sizing: border-box;
}
.est-17 [hidden] {
display: none !important;
}
.est-17__vh {
position: absolute;
inline-size: 1px;
block-size: 1px;
overflow: hidden;
clip-path: inset(50%);
white-space: nowrap;
}
@supports (color: oklch(0.7 0.1 300)) {
.est-17 {
--bg: oklch(0.974 0.005 110);
--surface: oklch(1 0 0);
--sunk: oklch(0.955 0.006 110);
--line: oklch(0.918 0.006 110);
--line-2: oklch(0.858 0.008 110);
--ink: oklch(0.19 0.008 120);
--muted: oklch(0.465 0.01 120);
--faint: oklch(0.615 0.011 120);
--accent: oklch(0.4 0.075 168);
--focus: oklch(0.52 0.19 258);
}
}
.est-17__skip {
position: absolute;
inset-block-start: .5rem;
inset-inline-start: .5rem;
z-index: 9;
translate: 0 -160%;
padding: .6rem .9rem;
font-size: .85rem;
font-weight: 500;
color: #fff;
background: var(--accent);
border-radius: 8px;
text-decoration: none;
transition: translate .18s ease;
}
.est-17__skip:focus-visible {
translate: 0 0;
outline: 3px solid var(--focus);
outline-offset: 2px;
}
.est-17__shell {
inline-size: min(60rem,100%);
justify-self: center;
display: grid;
grid-template-rows: auto minmax(0,1fr);
align-content: start;
}
.est-17__top {
display: flex;
flex-wrap: wrap;
gap: .75rem 1.75rem;
align-items: baseline;
padding: 1.15rem clamp(1rem,3vw,2rem);
border-block-end: 1px solid var(--line);
}
.est-17__brand {
font-size: 1rem;
font-weight: 600;
letter-spacing: -.025em;
}
.est-17__nav {
display: flex;
gap: 1.15rem;
}
.est-17__nav a {
font-size: .85rem;
color: var(--muted);
text-decoration: none;
border-block-end: 1px solid transparent;
transition: color .16s, border-color .16s;
}
.est-17__nav a:hover {
color: var(--ink);
border-color: var(--accent);
}
.est-17__main {
padding: clamp(1.5rem,4vw,3rem) clamp(1rem,3vw,2rem);
display: grid;
gap: 1.5rem;
align-content: start;
}
.est-17__form {
display: grid;
gap: .5rem;
max-inline-size: 34rem;
}
.est-17__label {
font-size: .9rem;
font-weight: 600;
letter-spacing: -.015em;
}
.est-17__row {
display: flex;
flex-wrap: wrap;
gap: .5rem;
}
.est-17__input {
flex: 1 1 15rem;
min-inline-size: 0;
min-block-size: 2.9rem;
padding: .7rem .9rem;
font: inherit;
font-size: .92rem;
color: var(--ink);
background: var(--surface);
border: 1px solid var(--line-2);
border-radius: 10px;
outline: none;
}
.est-17__input:focus-visible {
border-color: var(--focus);
box-shadow: 0 0 0 3px color-mix(in oklab, var(--focus) 22%, transparent);
}
.est-17__go {
min-block-size: 2.9rem;
padding: .7rem 1.15rem;
font: inherit;
font-size: .88rem;
font-weight: 500;
cursor: pointer;
color: #fff;
background: var(--accent);
border: 1px solid transparent;
border-radius: 10px;
transition: translate .16s, background .16s;
}
.est-17__go:hover {
translate: 0 -1px;
background: color-mix(in oklab, var(--accent) 88%, #000);
}
.est-17__hint {
margin: 0;
font-size: .8rem;
color: var(--faint);
}
.est-17__results {
display: grid;
gap: 1rem;
}
.est-17__results:focus-visible {
outline: 3px solid var(--focus);
outline-offset: 6px;
border-radius: 12px;
}
.est-17__rh {
margin: 0;
font-family: 'Geist Mono',ui-monospace,monospace;
font-size: .7rem;
font-weight: 500;
letter-spacing: .12em;
text-transform: uppercase;
color: var(--faint);
}
.est-17__empty {
display: grid;
justify-items: center;
text-align: center;
gap: .5rem;
padding: clamp(2rem,6vh,3.25rem) 1.5rem;
background: var(--surface);
border: 1px solid var(--line);
border-radius: 18px;
box-shadow: 0 1px 2px rgba(25,26,23,.04);
animation: est-17-in .45s cubic-bezier(.2,.7,.3,1) both;
}
@keyframes est-17-in {
from {
opacity: 0;
translate: 0 6px;
}
to {
opacity: 1;
translate: 0 0;
}
}
.est-17__glyph {
inline-size: 3.4rem;
block-size: 3.4rem;
display: grid;
place-items: center;
margin-block-end: .4rem;
color: var(--accent);
background: color-mix(in oklab, var(--accent) 8%, var(--surface));
border-radius: 14px;
box-shadow: 0 0 0 1px color-mix(in oklab, var(--accent) 18%, transparent);
}
.est-17__glyph svg {
inline-size: 1.45rem;
block-size: 1.45rem;
}
.est-17__eh {
margin: 0;
font-size: 1.08rem;
font-weight: 600;
letter-spacing: -.022em;
text-wrap: balance;
}
.est-17__ep {
margin: 0;
max-inline-size: 48ch;
font-size: .89rem;
line-height: 1.6;
color: var(--muted);
text-wrap: pretty;
}
.est-17__tries {
margin: .9rem 0 0;
padding: 0;
list-style: none;
display: flex;
flex-wrap: wrap;
gap: .5rem;
justify-content: center;
}
.est-17__try {
min-block-size: 2.6rem;
padding: .6rem .9rem;
font: inherit;
font-family: 'Geist Mono',ui-monospace,monospace;
font-size: .8rem;
cursor: pointer;
color: var(--ink);
background: var(--sunk);
border: 1px solid var(--line-2);
border-radius: 9px;
transition: background .16s, border-color .16s;
}
.est-17__try:hover {
background: color-mix(in oklab, var(--accent) 10%, var(--sunk));
border-color: var(--accent);
}
.est-17__try:focus-visible,
.est-17__go:focus-visible,
.est-17__nav a:focus-visible {
outline: 3px solid var(--focus);
outline-offset: 2px;
}
.est-17__list {
margin: 0;
padding: 0;
list-style: none;
display: grid;
gap: .5rem;
}
.est-17__item {
display: grid;
gap: .25rem;
padding: .9rem 1.05rem;
background: var(--surface);
border: 1px solid var(--line);
border-radius: 13px;
animation: est-17-in .4s cubic-bezier(.2,.7,.3,1) both;
}
.est-17__it {
font-size: .93rem;
font-weight: 500;
letter-spacing: -.015em;
}
.est-17__im {
font-family: 'Geist Mono',ui-monospace,monospace;
font-size: .74rem;
color: var(--faint);
}
@media (prefers-reduced-motion: reduce) {
.est-17 *,
.est-17 *::before,
.est-17 *::after {
animation: none !important;
transition: none !important;
}
}
@media (forced-colors: active) {
.est-17__empty,
.est-17__item,
.est-17__go,
.est-17__try {
border-color: CanvasText;
}
}(function(){
var root = document.querySelector('.est-17'); if(!root) return;
var form = root.querySelector('#est-17-form'), input = root.querySelector('#est-17-q'),
live = root.querySelector('#est-17-live'), empty = root.querySelector('#est-17-empty'),
list = root.querySelector('#est-17-list'), results = root.querySelector('#est-17-results');
var catalogue = [
['Tidal survey of the Exe estuary','SURV-1974-118 · Devon · 1974'],
['Coastal profile, Teignmouth to Dawlish','SURV-1974-121 · Devon · 1974'],
['Sheet 118, first edition','MAP-118-1 · Devon · 1969']
];
function announce(msg){ live.textContent = ''; setTimeout(function(){ live.textContent = msg; }, 40); }
function run(q){
var term = q.trim().toLowerCase();
var hits = term.length < 2 ? [] : catalogue.filter(function(r){
return (r[0] + ' ' + r[1]).toLowerCase().replace(/·/g,'').indexOf(term) > -1;
});
if(hits.length){
empty.hidden = true; list.hidden = false;
list.innerHTML = hits.map(function(r){
return '<li class="est-17__item"><span class="est-17__it">' + r[0] + '</span><span class="est-17__im">' + r[1] + '</span></li>';
}).join('');
announce(hits.length + (hits.length === 1 ? ' entry found for ' : ' entries found for ') + q);
} else {
empty.hidden = false; list.hidden = true; list.innerHTML = '';
announce('No catalogue entries for ' + q + '. Three suggested searches are listed.');
}
results.focus();
}
form.addEventListener('submit', function(e){ e.preventDefault(); run(input.value); });
root.addEventListener('click', function(e){
var t = e.target.closest('[data-q]'); if(!t) return;
input.value = t.dataset.q; run(t.dataset.q);
});
announce('No catalogue entries for ' + input.value + '. Three suggested searches are listed.');
})();(function(){
var root = document.querySelector('.est-17'); if(!root) return;
var form = root.querySelector('#est-17-form'), input = root.querySelector('#est-17-q'),
live = root.querySelector('#est-17-live'), empty = root.querySelector('#est-17-empty'),
list = root.querySelector('#est-17-list'), results = root.querySelector('#est-17-results');
var catalogue = [
['Tidal survey of the Exe estuary','SURV-1974-118 · Devon · 1974'],
['Coastal profile, Teignmouth to Dawlish','SURV-1974-121 · Devon · 1974'],
['Sheet 118, first edition','MAP-118-1 · Devon · 1969']
];
function announce(msg){ live.textContent = ''; setTimeout(function(){ live.textContent = msg; }, 40); }
function run(q){
var term = q.trim().toLowerCase();
var hits = term.length < 2 ? [] : catalogue.filter(function(r){
return (r[0] + ' ' + r[1]).toLowerCase().replace(/·/g,'').indexOf(term) > -1;
});
if(hits.length){
empty.hidden = true; list.hidden = false;
list.innerHTML = hits.map(function(r){
return '<li class="est-17__item"><span class="est-17__it">' + r[0] + '</span><span class="est-17__im">' + r[1] + '</span></li>';
}).join('');
announce(hits.length + (hits.length === 1 ? ' entry found for ' : ' entries found for ') + q);
} else {
empty.hidden = false; list.hidden = true; list.innerHTML = '';
announce('No catalogue entries for ' + q + '. Three suggested searches are listed.');
}
results.focus();
}
form.addEventListener('submit', function(e){ e.preventDefault(); run(input.value); });
root.addEventListener('click', function(e){
var t = e.target.closest('[data-q]'); if(!t) return;
input.value = t.dataset.q; run(t.dataset.q);
});
announce('No catalogue entries for ' + input.value + '. Three suggested searches are listed.');
})();Here's a working CSS Empty State from CodeFronts. Use it as-is or adapt to your framework. All classes are scoped under a unique prefix so the code won't collide with your existing styles. MIT licensed.
Demo: Accessible Empty State (ARIA Live)
Source: https://codefronts.com/snippets/css-empty-states/accessible-empty-state-aria-live/
The accessible version of a no-results state, in a library catalogue: a visually hidden role="status" region announces the outcome, focus moves to the results container so a screen reader lands on the message rather than hunting for it, and the recovery suggestions are real buttons that re-run the search. Nothing about the accessibility work is visible — which is the point.
## HTML
```html
<div class="est-17">
<a class="est-17__skip" href="#est-17-results">Skip to results</a>
<div class="est-17__shell">
<header class="est-17__top">
<span class="est-17__brand">Ordnance Library</span>
<nav class="est-17__nav" aria-label="Catalogue">
<a href="#est-17">Maps</a>
<a href="#est-17">Surveys</a>
<a href="#est-17">Photographs</a>
</nav>
</header>
<main class="est-17__main">
<form class="est-17__form" id="est-17-form" role="search">
<label class="est-17__label" for="est-17-q">Search the catalogue</label>
<div class="est-17__row">
<input class="est-17__input" type="search" id="est-17-q" name="q" value="tidal survey 1974" autocomplete="off"
aria-describedby="est-17-hint" aria-controls="est-17-results">
<button class="est-17__go" type="submit">Search</button>
</div>
<p class="est-17__hint" id="est-17-hint">Type at least two characters. Results update when you press Search.</p>
</form>
<div class="est-17__vh" role="status" aria-live="polite" aria-atomic="true" id="est-17-live"></div>
<section class="est-17__results" id="est-17-results" tabindex="-1" aria-labelledby="est-17-rh">
<h2 class="est-17__rh" id="est-17-rh">Results</h2>
<div class="est-17__empty" id="est-17-empty">
<span class="est-17__glyph" aria-hidden="true">
<svg viewBox="0 0 24 24" fill="none" focusable="false"><rect x="4" y="5" width="16" height="14" rx="2.2" stroke="currentColor" stroke-width="1.6"/><path d="M8 9.5h8M8 13h5" stroke="currentColor" stroke-width="1.6" stroke-linecap="round"/></svg>
</span>
<p class="est-17__eh">No catalogue entries for that phrase</p>
<p class="est-17__ep">Sheet numbers are indexed without spaces, and surveys before 1980 are filed by county rather than year.</p>
<ul class="est-17__tries">
<li><button class="est-17__try" type="button" data-q="tidal survey">tidal survey</button></li>
<li><button class="est-17__try" type="button" data-q="devon county 1974">devon county 1974</button></li>
<li><button class="est-17__try" type="button" data-q="sheet 118">sheet 118</button></li>
</ul>
</div>
<ol class="est-17__list" id="est-17-list" hidden></ol>
</section>
</main>
</div>
</div>
```
## CSS
```css
@import url('https://fonts.googleapis.com/css2?family=Geist:wght@400;500;600&family=Geist+Mono:wght@400;500&display=swap');
.est-17 {
--bg: #f7f7f4;
--surface: #ffffff;
--sunk: #eff0eb;
--line: #e4e5df;
--line-2: #d1d2ca;
--ink: #191a17;
--muted: #585a53;
--faint: #878980;
--accent: #1f5c4d;
--focus: #0b62d6;
position: relative;
width: 100%;
min-height: 100vh;
min-height: 100svh;
display: grid;
box-sizing: border-box;
background: var(--bg);
color: var(--ink);
color-scheme: light;
font-synthesis: none;
font-family: 'Geist',ui-sans-serif,system-ui,-apple-system,sans-serif;
-webkit-font-smoothing: antialiased;
}
.est-17 *,
.est-17 *::before,
.est-17 *::after {
box-sizing: border-box;
}
.est-17 [hidden] {
display: none !important;
}
.est-17__vh {
position: absolute;
inline-size: 1px;
block-size: 1px;
overflow: hidden;
clip-path: inset(50%);
white-space: nowrap;
}
@supports (color: oklch(0.7 0.1 300)) {
.est-17 {
--bg: oklch(0.974 0.005 110);
--surface: oklch(1 0 0);
--sunk: oklch(0.955 0.006 110);
--line: oklch(0.918 0.006 110);
--line-2: oklch(0.858 0.008 110);
--ink: oklch(0.19 0.008 120);
--muted: oklch(0.465 0.01 120);
--faint: oklch(0.615 0.011 120);
--accent: oklch(0.4 0.075 168);
--focus: oklch(0.52 0.19 258);
}
}
.est-17__skip {
position: absolute;
inset-block-start: .5rem;
inset-inline-start: .5rem;
z-index: 9;
translate: 0 -160%;
padding: .6rem .9rem;
font-size: .85rem;
font-weight: 500;
color: #fff;
background: var(--accent);
border-radius: 8px;
text-decoration: none;
transition: translate .18s ease;
}
.est-17__skip:focus-visible {
translate: 0 0;
outline: 3px solid var(--focus);
outline-offset: 2px;
}
.est-17__shell {
inline-size: min(60rem,100%);
justify-self: center;
display: grid;
grid-template-rows: auto minmax(0,1fr);
align-content: start;
}
.est-17__top {
display: flex;
flex-wrap: wrap;
gap: .75rem 1.75rem;
align-items: baseline;
padding: 1.15rem clamp(1rem,3vw,2rem);
border-block-end: 1px solid var(--line);
}
.est-17__brand {
font-size: 1rem;
font-weight: 600;
letter-spacing: -.025em;
}
.est-17__nav {
display: flex;
gap: 1.15rem;
}
.est-17__nav a {
font-size: .85rem;
color: var(--muted);
text-decoration: none;
border-block-end: 1px solid transparent;
transition: color .16s, border-color .16s;
}
.est-17__nav a:hover {
color: var(--ink);
border-color: var(--accent);
}
.est-17__main {
padding: clamp(1.5rem,4vw,3rem) clamp(1rem,3vw,2rem);
display: grid;
gap: 1.5rem;
align-content: start;
}
.est-17__form {
display: grid;
gap: .5rem;
max-inline-size: 34rem;
}
.est-17__label {
font-size: .9rem;
font-weight: 600;
letter-spacing: -.015em;
}
.est-17__row {
display: flex;
flex-wrap: wrap;
gap: .5rem;
}
.est-17__input {
flex: 1 1 15rem;
min-inline-size: 0;
min-block-size: 2.9rem;
padding: .7rem .9rem;
font: inherit;
font-size: .92rem;
color: var(--ink);
background: var(--surface);
border: 1px solid var(--line-2);
border-radius: 10px;
outline: none;
}
.est-17__input:focus-visible {
border-color: var(--focus);
box-shadow: 0 0 0 3px color-mix(in oklab, var(--focus) 22%, transparent);
}
.est-17__go {
min-block-size: 2.9rem;
padding: .7rem 1.15rem;
font: inherit;
font-size: .88rem;
font-weight: 500;
cursor: pointer;
color: #fff;
background: var(--accent);
border: 1px solid transparent;
border-radius: 10px;
transition: translate .16s, background .16s;
}
.est-17__go:hover {
translate: 0 -1px;
background: color-mix(in oklab, var(--accent) 88%, #000);
}
.est-17__hint {
margin: 0;
font-size: .8rem;
color: var(--faint);
}
.est-17__results {
display: grid;
gap: 1rem;
}
.est-17__results:focus-visible {
outline: 3px solid var(--focus);
outline-offset: 6px;
border-radius: 12px;
}
.est-17__rh {
margin: 0;
font-family: 'Geist Mono',ui-monospace,monospace;
font-size: .7rem;
font-weight: 500;
letter-spacing: .12em;
text-transform: uppercase;
color: var(--faint);
}
.est-17__empty {
display: grid;
justify-items: center;
text-align: center;
gap: .5rem;
padding: clamp(2rem,6vh,3.25rem) 1.5rem;
background: var(--surface);
border: 1px solid var(--line);
border-radius: 18px;
box-shadow: 0 1px 2px rgba(25,26,23,.04);
animation: est-17-in .45s cubic-bezier(.2,.7,.3,1) both;
}
@keyframes est-17-in {
from {
opacity: 0;
translate: 0 6px;
}
to {
opacity: 1;
translate: 0 0;
}
}
.est-17__glyph {
inline-size: 3.4rem;
block-size: 3.4rem;
display: grid;
place-items: center;
margin-block-end: .4rem;
color: var(--accent);
background: color-mix(in oklab, var(--accent) 8%, var(--surface));
border-radius: 14px;
box-shadow: 0 0 0 1px color-mix(in oklab, var(--accent) 18%, transparent);
}
.est-17__glyph svg {
inline-size: 1.45rem;
block-size: 1.45rem;
}
.est-17__eh {
margin: 0;
font-size: 1.08rem;
font-weight: 600;
letter-spacing: -.022em;
text-wrap: balance;
}
.est-17__ep {
margin: 0;
max-inline-size: 48ch;
font-size: .89rem;
line-height: 1.6;
color: var(--muted);
text-wrap: pretty;
}
.est-17__tries {
margin: .9rem 0 0;
padding: 0;
list-style: none;
display: flex;
flex-wrap: wrap;
gap: .5rem;
justify-content: center;
}
.est-17__try {
min-block-size: 2.6rem;
padding: .6rem .9rem;
font: inherit;
font-family: 'Geist Mono',ui-monospace,monospace;
font-size: .8rem;
cursor: pointer;
color: var(--ink);
background: var(--sunk);
border: 1px solid var(--line-2);
border-radius: 9px;
transition: background .16s, border-color .16s;
}
.est-17__try:hover {
background: color-mix(in oklab, var(--accent) 10%, var(--sunk));
border-color: var(--accent);
}
.est-17__try:focus-visible,
.est-17__go:focus-visible,
.est-17__nav a:focus-visible {
outline: 3px solid var(--focus);
outline-offset: 2px;
}
.est-17__list {
margin: 0;
padding: 0;
list-style: none;
display: grid;
gap: .5rem;
}
.est-17__item {
display: grid;
gap: .25rem;
padding: .9rem 1.05rem;
background: var(--surface);
border: 1px solid var(--line);
border-radius: 13px;
animation: est-17-in .4s cubic-bezier(.2,.7,.3,1) both;
}
.est-17__it {
font-size: .93rem;
font-weight: 500;
letter-spacing: -.015em;
}
.est-17__im {
font-family: 'Geist Mono',ui-monospace,monospace;
font-size: .74rem;
color: var(--faint);
}
@media (prefers-reduced-motion: reduce) {
.est-17 *,
.est-17 *::before,
.est-17 *::after {
animation: none !important;
transition: none !important;
}
}
@media (forced-colors: active) {
.est-17__empty,
.est-17__item,
.est-17__go,
.est-17__try {
border-color: CanvasText;
}
}
```
## JavaScript
```js
(function(){
var root = document.querySelector('.est-17'); if(!root) return;
var form = root.querySelector('#est-17-form'), input = root.querySelector('#est-17-q'),
live = root.querySelector('#est-17-live'), empty = root.querySelector('#est-17-empty'),
list = root.querySelector('#est-17-list'), results = root.querySelector('#est-17-results');
var catalogue = [
['Tidal survey of the Exe estuary','SURV-1974-118 · Devon · 1974'],
['Coastal profile, Teignmouth to Dawlish','SURV-1974-121 · Devon · 1974'],
['Sheet 118, first edition','MAP-118-1 · Devon · 1969']
];
function announce(msg){ live.textContent = ''; setTimeout(function(){ live.textContent = msg; }, 40); }
function run(q){
var term = q.trim().toLowerCase();
var hits = term.length < 2 ? [] : catalogue.filter(function(r){
return (r[0] + ' ' + r[1]).toLowerCase().replace(/·/g,'').indexOf(term) > -1;
});
if(hits.length){
empty.hidden = true; list.hidden = false;
list.innerHTML = hits.map(function(r){
return '<li class="est-17__item"><span class="est-17__it">' + r[0] + '</span><span class="est-17__im">' + r[1] + '</span></li>';
}).join('');
announce(hits.length + (hits.length === 1 ? ' entry found for ' : ' entries found for ') + q);
} else {
empty.hidden = false; list.hidden = true; list.innerHTML = '';
announce('No catalogue entries for ' + q + '. Three suggested searches are listed.');
}
results.focus();
}
form.addEventListener('submit', function(e){ e.preventDefault(); run(input.value); });
root.addEventListener('click', function(e){
var t = e.target.closest('[data-q]'); if(!t) return;
input.value = t.dataset.q; run(t.dataset.q);
});
announce('No catalogue entries for ' + input.value + '. Three suggested searches are listed.');
})();
```Here's a working CSS Empty State from CodeFronts. Use it as-is or adapt to your framework. All classes are scoped under a unique prefix so the code won't collide with your existing styles. MIT licensed.
Demo: Accessible Empty State (ARIA Live)
Source: https://codefronts.com/snippets/css-empty-states/accessible-empty-state-aria-live/
The accessible version of a no-results state, in a library catalogue: a visually hidden role="status" region announces the outcome, focus moves to the results container so a screen reader lands on the message rather than hunting for it, and the recovery suggestions are real buttons that re-run the search. Nothing about the accessibility work is visible — which is the point.
## HTML
```html
<div class="est-17">
<a class="est-17__skip" href="#est-17-results">Skip to results</a>
<div class="est-17__shell">
<header class="est-17__top">
<span class="est-17__brand">Ordnance Library</span>
<nav class="est-17__nav" aria-label="Catalogue">
<a href="#est-17">Maps</a>
<a href="#est-17">Surveys</a>
<a href="#est-17">Photographs</a>
</nav>
</header>
<main class="est-17__main">
<form class="est-17__form" id="est-17-form" role="search">
<label class="est-17__label" for="est-17-q">Search the catalogue</label>
<div class="est-17__row">
<input class="est-17__input" type="search" id="est-17-q" name="q" value="tidal survey 1974" autocomplete="off"
aria-describedby="est-17-hint" aria-controls="est-17-results">
<button class="est-17__go" type="submit">Search</button>
</div>
<p class="est-17__hint" id="est-17-hint">Type at least two characters. Results update when you press Search.</p>
</form>
<div class="est-17__vh" role="status" aria-live="polite" aria-atomic="true" id="est-17-live"></div>
<section class="est-17__results" id="est-17-results" tabindex="-1" aria-labelledby="est-17-rh">
<h2 class="est-17__rh" id="est-17-rh">Results</h2>
<div class="est-17__empty" id="est-17-empty">
<span class="est-17__glyph" aria-hidden="true">
<svg viewBox="0 0 24 24" fill="none" focusable="false"><rect x="4" y="5" width="16" height="14" rx="2.2" stroke="currentColor" stroke-width="1.6"/><path d="M8 9.5h8M8 13h5" stroke="currentColor" stroke-width="1.6" stroke-linecap="round"/></svg>
</span>
<p class="est-17__eh">No catalogue entries for that phrase</p>
<p class="est-17__ep">Sheet numbers are indexed without spaces, and surveys before 1980 are filed by county rather than year.</p>
<ul class="est-17__tries">
<li><button class="est-17__try" type="button" data-q="tidal survey">tidal survey</button></li>
<li><button class="est-17__try" type="button" data-q="devon county 1974">devon county 1974</button></li>
<li><button class="est-17__try" type="button" data-q="sheet 118">sheet 118</button></li>
</ul>
</div>
<ol class="est-17__list" id="est-17-list" hidden></ol>
</section>
</main>
</div>
</div>
```
## CSS
```css
@import url('https://fonts.googleapis.com/css2?family=Geist:wght@400;500;600&family=Geist+Mono:wght@400;500&display=swap');
.est-17 {
--bg: #f7f7f4;
--surface: #ffffff;
--sunk: #eff0eb;
--line: #e4e5df;
--line-2: #d1d2ca;
--ink: #191a17;
--muted: #585a53;
--faint: #878980;
--accent: #1f5c4d;
--focus: #0b62d6;
position: relative;
width: 100%;
min-height: 100vh;
min-height: 100svh;
display: grid;
box-sizing: border-box;
background: var(--bg);
color: var(--ink);
color-scheme: light;
font-synthesis: none;
font-family: 'Geist',ui-sans-serif,system-ui,-apple-system,sans-serif;
-webkit-font-smoothing: antialiased;
}
.est-17 *,
.est-17 *::before,
.est-17 *::after {
box-sizing: border-box;
}
.est-17 [hidden] {
display: none !important;
}
.est-17__vh {
position: absolute;
inline-size: 1px;
block-size: 1px;
overflow: hidden;
clip-path: inset(50%);
white-space: nowrap;
}
@supports (color: oklch(0.7 0.1 300)) {
.est-17 {
--bg: oklch(0.974 0.005 110);
--surface: oklch(1 0 0);
--sunk: oklch(0.955 0.006 110);
--line: oklch(0.918 0.006 110);
--line-2: oklch(0.858 0.008 110);
--ink: oklch(0.19 0.008 120);
--muted: oklch(0.465 0.01 120);
--faint: oklch(0.615 0.011 120);
--accent: oklch(0.4 0.075 168);
--focus: oklch(0.52 0.19 258);
}
}
.est-17__skip {
position: absolute;
inset-block-start: .5rem;
inset-inline-start: .5rem;
z-index: 9;
translate: 0 -160%;
padding: .6rem .9rem;
font-size: .85rem;
font-weight: 500;
color: #fff;
background: var(--accent);
border-radius: 8px;
text-decoration: none;
transition: translate .18s ease;
}
.est-17__skip:focus-visible {
translate: 0 0;
outline: 3px solid var(--focus);
outline-offset: 2px;
}
.est-17__shell {
inline-size: min(60rem,100%);
justify-self: center;
display: grid;
grid-template-rows: auto minmax(0,1fr);
align-content: start;
}
.est-17__top {
display: flex;
flex-wrap: wrap;
gap: .75rem 1.75rem;
align-items: baseline;
padding: 1.15rem clamp(1rem,3vw,2rem);
border-block-end: 1px solid var(--line);
}
.est-17__brand {
font-size: 1rem;
font-weight: 600;
letter-spacing: -.025em;
}
.est-17__nav {
display: flex;
gap: 1.15rem;
}
.est-17__nav a {
font-size: .85rem;
color: var(--muted);
text-decoration: none;
border-block-end: 1px solid transparent;
transition: color .16s, border-color .16s;
}
.est-17__nav a:hover {
color: var(--ink);
border-color: var(--accent);
}
.est-17__main {
padding: clamp(1.5rem,4vw,3rem) clamp(1rem,3vw,2rem);
display: grid;
gap: 1.5rem;
align-content: start;
}
.est-17__form {
display: grid;
gap: .5rem;
max-inline-size: 34rem;
}
.est-17__label {
font-size: .9rem;
font-weight: 600;
letter-spacing: -.015em;
}
.est-17__row {
display: flex;
flex-wrap: wrap;
gap: .5rem;
}
.est-17__input {
flex: 1 1 15rem;
min-inline-size: 0;
min-block-size: 2.9rem;
padding: .7rem .9rem;
font: inherit;
font-size: .92rem;
color: var(--ink);
background: var(--surface);
border: 1px solid var(--line-2);
border-radius: 10px;
outline: none;
}
.est-17__input:focus-visible {
border-color: var(--focus);
box-shadow: 0 0 0 3px color-mix(in oklab, var(--focus) 22%, transparent);
}
.est-17__go {
min-block-size: 2.9rem;
padding: .7rem 1.15rem;
font: inherit;
font-size: .88rem;
font-weight: 500;
cursor: pointer;
color: #fff;
background: var(--accent);
border: 1px solid transparent;
border-radius: 10px;
transition: translate .16s, background .16s;
}
.est-17__go:hover {
translate: 0 -1px;
background: color-mix(in oklab, var(--accent) 88%, #000);
}
.est-17__hint {
margin: 0;
font-size: .8rem;
color: var(--faint);
}
.est-17__results {
display: grid;
gap: 1rem;
}
.est-17__results:focus-visible {
outline: 3px solid var(--focus);
outline-offset: 6px;
border-radius: 12px;
}
.est-17__rh {
margin: 0;
font-family: 'Geist Mono',ui-monospace,monospace;
font-size: .7rem;
font-weight: 500;
letter-spacing: .12em;
text-transform: uppercase;
color: var(--faint);
}
.est-17__empty {
display: grid;
justify-items: center;
text-align: center;
gap: .5rem;
padding: clamp(2rem,6vh,3.25rem) 1.5rem;
background: var(--surface);
border: 1px solid var(--line);
border-radius: 18px;
box-shadow: 0 1px 2px rgba(25,26,23,.04);
animation: est-17-in .45s cubic-bezier(.2,.7,.3,1) both;
}
@keyframes est-17-in {
from {
opacity: 0;
translate: 0 6px;
}
to {
opacity: 1;
translate: 0 0;
}
}
.est-17__glyph {
inline-size: 3.4rem;
block-size: 3.4rem;
display: grid;
place-items: center;
margin-block-end: .4rem;
color: var(--accent);
background: color-mix(in oklab, var(--accent) 8%, var(--surface));
border-radius: 14px;
box-shadow: 0 0 0 1px color-mix(in oklab, var(--accent) 18%, transparent);
}
.est-17__glyph svg {
inline-size: 1.45rem;
block-size: 1.45rem;
}
.est-17__eh {
margin: 0;
font-size: 1.08rem;
font-weight: 600;
letter-spacing: -.022em;
text-wrap: balance;
}
.est-17__ep {
margin: 0;
max-inline-size: 48ch;
font-size: .89rem;
line-height: 1.6;
color: var(--muted);
text-wrap: pretty;
}
.est-17__tries {
margin: .9rem 0 0;
padding: 0;
list-style: none;
display: flex;
flex-wrap: wrap;
gap: .5rem;
justify-content: center;
}
.est-17__try {
min-block-size: 2.6rem;
padding: .6rem .9rem;
font: inherit;
font-family: 'Geist Mono',ui-monospace,monospace;
font-size: .8rem;
cursor: pointer;
color: var(--ink);
background: var(--sunk);
border: 1px solid var(--line-2);
border-radius: 9px;
transition: background .16s, border-color .16s;
}
.est-17__try:hover {
background: color-mix(in oklab, var(--accent) 10%, var(--sunk));
border-color: var(--accent);
}
.est-17__try:focus-visible,
.est-17__go:focus-visible,
.est-17__nav a:focus-visible {
outline: 3px solid var(--focus);
outline-offset: 2px;
}
.est-17__list {
margin: 0;
padding: 0;
list-style: none;
display: grid;
gap: .5rem;
}
.est-17__item {
display: grid;
gap: .25rem;
padding: .9rem 1.05rem;
background: var(--surface);
border: 1px solid var(--line);
border-radius: 13px;
animation: est-17-in .4s cubic-bezier(.2,.7,.3,1) both;
}
.est-17__it {
font-size: .93rem;
font-weight: 500;
letter-spacing: -.015em;
}
.est-17__im {
font-family: 'Geist Mono',ui-monospace,monospace;
font-size: .74rem;
color: var(--faint);
}
@media (prefers-reduced-motion: reduce) {
.est-17 *,
.est-17 *::before,
.est-17 *::after {
animation: none !important;
transition: none !important;
}
}
@media (forced-colors: active) {
.est-17__empty,
.est-17__item,
.est-17__go,
.est-17__try {
border-color: CanvasText;
}
}
```
## JavaScript
```js
(function(){
var root = document.querySelector('.est-17'); if(!root) return;
var form = root.querySelector('#est-17-form'), input = root.querySelector('#est-17-q'),
live = root.querySelector('#est-17-live'), empty = root.querySelector('#est-17-empty'),
list = root.querySelector('#est-17-list'), results = root.querySelector('#est-17-results');
var catalogue = [
['Tidal survey of the Exe estuary','SURV-1974-118 · Devon · 1974'],
['Coastal profile, Teignmouth to Dawlish','SURV-1974-121 · Devon · 1974'],
['Sheet 118, first edition','MAP-118-1 · Devon · 1969']
];
function announce(msg){ live.textContent = ''; setTimeout(function(){ live.textContent = msg; }, 40); }
function run(q){
var term = q.trim().toLowerCase();
var hits = term.length < 2 ? [] : catalogue.filter(function(r){
return (r[0] + ' ' + r[1]).toLowerCase().replace(/·/g,'').indexOf(term) > -1;
});
if(hits.length){
empty.hidden = true; list.hidden = false;
list.innerHTML = hits.map(function(r){
return '<li class="est-17__item"><span class="est-17__it">' + r[0] + '</span><span class="est-17__im">' + r[1] + '</span></li>';
}).join('');
announce(hits.length + (hits.length === 1 ? ' entry found for ' : ' entries found for ') + q);
} else {
empty.hidden = false; list.hidden = true; list.innerHTML = '';
announce('No catalogue entries for ' + q + '. Three suggested searches are listed.');
}
results.focus();
}
form.addEventListener('submit', function(e){ e.preventDefault(); run(input.value); });
root.addEventListener('click', function(e){
var t = e.target.closest('[data-q]'); if(!t) return;
input.value = t.dataset.q; run(t.dataset.q);
});
announce('No catalogue entries for ' + input.value + '. Three suggested searches are listed.');
})();
```How this works
Announce the outcome, not the mechanism. The live region is empty in the DOM and filled after every search, including the count and the query:
<div class="est-17__vh" role="status" aria-live="polite" aria-atomic="true" id="est-17-live"></div>function announce(msg){
live.textContent = '';
setTimeout(function(){ live.textContent = msg; }, 40);
}The clear-then-set is not superstition: identical text written twice is not announced twice, and repeated failed searches are exactly when a user needs the confirmation.
Move focus to the answer. The results container takes focus so the next thing read is the message:
<section class="est-17__results" id="est-17-results" tabindex="-1" aria-labelledby="est-17-rh">
results.focus();
.est-17__results:focus-visible{ outline:3px solid var(--focus); outline-offset:6px }Suggestions must be controls. Styled spans are invisible to assistive tech. These are buttons that fill the input and re-run the search:
<button class="est-17__try" type="button" data-q="devon county 1974">devon county 1974</button>Say why, in domain terms. The copy explains that sheet numbers are indexed without spaces and pre-1980 surveys are filed by county — the actual reason the search failed, which is worth more than any amount of ARIA.
Make it yours
- Announce the count for successful searches too — three entries found is as useful as the failure
- For search-as-you-type, debounce to at least 500ms before announcing, or the live region becomes noise
- Move focus to the first result instead of the container if your results are individually focusable
- Add
aria-busyon the region while a request is in flight for slow backends - Keep the hint text visible rather than only in
aria-describedby: sighted users need it as much as anyone - Swap the suggestion set for real query rewrites from your search engine
Gotchas — read before shipping
- A live region must exist in the DOM before you write to it. Inserting the region and its text together announces nothing
- Identical consecutive text is not re-announced. Clear it first, then set it on a short timeout
aria-live="assertive"interrupts the user mid-sentence. For search results,politeis correct- Moving focus needs
tabindex="-1"and a visible focus style, or sighted keyboard users lose their place silently - Never build suggestions from non-interactive elements. If it acts like a button, it must be a button
- Do not hide the empty state from screen readers because the live region already said it — users navigate back to it
Browser support
| Chrome | Safari | Firefox | Edge |
|---|---|---|---|
| 114+ | 17.5+ | 121+ | 114+ |
Floor set by oklch(), color-mix(), text-wrap as this demo is written. The core technique itself goes back further — Chrome 111+.
Live regions work in every current screen reader and browser pairing; the clear-then-set timeout is the pattern that makes repeat announcements reliable across VoiceOver, NVDA and JAWS. :focus-visible needs Chrome 86 / Safari 15.4 / Firefox 85. color-mix() and oklch() sit behind @supports with hex fallbacks.