A sighted user types into a search box and sees the results appear underneath. A screen reader user types the same thing and, on many sites, hears nothing at all. The results are there. Nobody told them.
We wanted to know which of the common fixes actually work, so we built nine versions of a search box and a "load more" list, scanned each with axe-core, and then ran every one in Safari with VoiceOver and recorded what it said. axe reported zero issues for all nine. VoiceOver said nothing useful for three of them, including one that sent the user back to the top of the page. This guide covers what a screen reader should hear when content changes, when to move focus instead, working code for each case, and how to test the part scanners can't hear.
TL;DR: When results load below a search box, keep focus in the box and announce a short count, such as "12 results for tea", from a
role="status"region that is already in the page before the results arrive. Wait until the user stops typing before you announce. Move focus only when the user asked for a new view, for example by pressing Enter or a Search button, and then move it to a heading that states the count. After "load more", move focus to the first new item and don't fire an announcement at the same moment. In our VoiceOver test, a live region added together with its message was never spoken, and arole="status"region that was already there was spoken every time.
What Should a Screen Reader Hear When Search Results Load?
A short status message with the number of results, not the results themselves. WCAG 2.2 calls this a status message: "change in content that is not a change of context, and that provides information to the user on the success or results of an action" (W3C, WCAG 2.2). Success criterion 4.1.3 Status Messages (Level AA) requires that these "can be presented to the user by assistive technologies without receiving focus".
W3C's own Understanding document uses search as its first example. After a user presses Search, the results appear below the button with the message "5 results returned", which "is given an appropriate role for a status message. A screen reader announces, 'Five results returned'" (W3C, Understanding SC 4.1.3, updated May 2026). The same page lists "Searching...", "18 results returned" and "No results returned" as status messages, as long as they don't take focus.
Announcing the count rather than the list is an old lesson. When GOV.UK rebuilt its search filters in 2014, wrapping the whole results list in a live region meant that "every time the user updates the checkboxes their screen reader is going to read out every single search result". The fix they adopted was to "update only the result count" (GOV.UK Technology blog, 2014).
Several other criteria apply to dynamic results (W3C, WCAG 2.2):
Criterion | Level | What it means here |
|---|---|---|
4.1.3 Status Messages | AA | Result counts, "Searching..." and "No results" are exposed through a status role without moving focus |
3.2.2 On Input | A | Typing in the box or ticking a filter doesn't move focus or change context unexpectedly |
2.4.3 Focus Order | A | When you do move focus, it lands somewhere that "preserves meaning and operability" |
1.3.1 Info and Relationships | A | Results are real lists with real headings, so users can skim them |
2.2.4 Interruptions | AAA | Users can postpone or suppress interruptions, which discourages assertive messages on every keystroke |
Do Accessibility Scanners Catch Silent Search Results?
No. We built nine versions on one test page: six ways to show search results and three ways to handle a "load more" button. Each search version used the same form and a simulated fetch; only the announcement changed. We scanned each one before and after the results loaded with axe-core 4.11.1 through @axe-core/playwright in Chrome, using the tags a typical WCAG 2.2 AA scan uses (wcag2a, wcag2aa, wcag21a, wcag21aa, wcag22aa).
Then we ran each version in Safari 27.2 on macOS 27.2 with VoiceOver, driven by Guidepup, which presses keys the way a user would and reads back VoiceOver's last spoken phrase. We polled that phrase every 100 milliseconds, typed "gr" into the search box, pressed Enter, and recorded everything VoiceOver said in the next seven seconds. For the version that updates as you type, we typed "green" without pressing Enter; for the "load more" versions, we pressed Enter on the button. Each version ran at least three times, and we discarded runs where the typing landed in Safari's address bar instead of the page.
The same results as a table:
Version | axe issues (default AA) | What VoiceOver said after the results loaded |
|---|---|---|
Results appear, no announcement | 0 | Nothing |
Live region added at the same time as its message | 0 | Nothing |
| 0 | "• Earl Grey • Green tea" (every result) |
Persistent | 0 | "2 results for gr" |
Focus moved to a results heading | 0 | "heading level 2, 2 results for gr" |
| 0 | "4 results", "2 results", "e", "1 results", "n" |
Load more: button removed after the last page | 0 | The page title, then "web content" |
Load more: focus moved to the first new item | 0 | "link, Green tea, list 5 items" |
Load more: focus moved plus a status message | 0 | "Showing all 5 teas" (link name not heard) |
Three results stand out. The live region that was added to the page at the same moment as its message stayed silent in every run, even though the markup looked correct. A region that was already in the page and then had its text changed was spoken in every run. And when the "load more" button removed itself after the last page, focus fell back to the document: VoiceOver announced the page title followed by "web content", and the user was back at the top with no idea where the new items were.
axe flagged none of it. That isn't a bug in axe. Deque's rule list has no rule for 4.1.3, and none that mentions live regions; the only checks that touch aria-live are general ones such as aria-valid-attr-value, which confirm the attribute's value is legal (axe-core rule descriptions). Whether a message is spoken depends on timing, and a scan of a finished page can't see timing. It's the same blind spot we found with date pickers that no keyboard can operate, and the reason automated testing is a floor, not a ceiling.
Citation capsule: In an October 2026 A11yFlow test of nine ways to show search and "load more" results, axe-core 4.11.1 reported zero WCAG 2.2 AA issues for all nine. In VoiceOver on Safari 27.2, a live region inserted together with its message was never announced, while a
role="status"region already present in the page announced "2 results for gr" in every run. Status messages fall under WCAG 2.2 SC 4.1.3, which needs a screen reader check because scanners can't observe announcement timing (W3C, WCAG 2.2).
Be clear about what this test does and doesn't show. It used one screen reader and one browser, and VoiceOver is the primary desktop screen reader of only 6.5% of respondents to WebAIM's 2026 survey (more on that below). Our capture read VoiceOver's last phrase every 100 milliseconds, so a phrase replaced faster than that could be missed; that matters for the last row, where we can say the link name wasn't recorded but not for certain that it was never started. Treat it as one well-controlled data point, not a support table.
Should You Announce the Results or Move Focus?
Announce when the user is still doing something; move focus when they asked to go somewhere. A user typing in a search box is mid-task. Moving their focus to the results while they type is a change of context under SC 3.2.2, and it takes their cursor out of the field they were typing in. A user who presses Enter or a Search button has asked to see results, so taking them there is expected.
Scott O'Hara draws the same line: for live filtering, keep the user in the field and announce, and when the user submits, "JavaScript can programmatically move focus to the heading that introduces the result listing" (Scott O'Hara, 2022).
Situation | Keep focus where it is? | What to do |
|---|---|---|
Results filter as you type | Yes | Announce the count after the user pauses |
Ticking a filter checkbox | Yes | Announce the new count |
Pressing Enter or a Search button | No | Move focus to a results heading that states the count (code in Pattern 2) |
Clicking "Load more" | No | Move focus to the first new item |
The last page loads and the button goes away | No | Move focus to the first new item before removing the button |
No results found | Yes | Announce "No results for ..." |
Saving a form in place | Yes | Announce "Saved" |
Don't do both at once. In our test, moving focus to the first new item on its own was spoken clearly as "link, Green tea, list 5 items". Adding a status message at the same moment meant VoiceOver spoke the status and we never recorded the link name, so the user heard that loading had finished but not where they now were. Andrea de Souza warns about exactly this: "Focus change and live regions firing at the same time are prone to conflicts" (HTMHell, 2023). If focus moves, let the focused element do the talking.
Pattern 1: A Status Region That Is Already There
The single most reliable fix is to put an empty status region in the page from the start and change its text later. MDN puts it directly: "The most reliable way to ensure that live regions are registered is to include them in the initial markup" (MDN, 2026). Scott O'Hara's cross-browser tests reached the same result: pre-existing empty regions "proves to be the most robust way", and injecting new region nodes "is consistently the least supported way" (Scott O'Hara, 2022). Our test agreed.
<form role="search" id="search">
<label for="q">Search teas</label>
<input id="q" name="q" type="search" aria-describedby="q-hint">
<p id="q-hint">Results update as you type.</p>
<button>Search</button>
</form>
<!-- In the page from the start, and never removed -->
<div id="search-status" role="status" aria-live="polite" class="visually-hidden"></div>
<!-- Its text becomes the count, such as "2 results for gr", when focus moves here -->
<h2 id="results-heading" tabindex="-1">Results</h2>
<ul id="results"></ul>role="status" already implies aria-live="polite" and aria-atomic="true" (W3C, WAI-ARIA 1.2), so by the spec you don't need both. MDN recommends adding the redundant aria-live="polite" anyway "to maximize compatibility" with older browser and screen reader pairings, and it does no harm. Don't add aria-live to a role="alert" region, though: MDN notes that combination "causes double speaking issues in VoiceOver on iOS".
Hide the region visually with a clip-based class, not display: none or the hidden attribute. Adrian Roselli's tests across JAWS, NVDA, Narrator, VoiceOver, Orca and TalkBack, published in January 2026, found that "No live region announces when hidden" (Adrian Roselli, 2026). Our status region used a standard clip-based visually-hidden class and was spoken every time.
The hint under the field matters too. Sara Soueidan recommends telling users up front that "Results will [filter / display / etc.] as you type", so the announcements don't come as a surprise (Sara Soueidan, 2024).
If you're working in React, the rule is the same: the region must be mounted before the message arrives. Render it unconditionally and change only its text. The mistake our test caught, {count && <div role="status">{count} results</div>}, is the live region that mounts with its content, and it was silent every time. (It also renders a stray "0" when there are no results.)
export function ResultsStatus({ message }) {
// Always rendered, so screen readers have registered it before the text changes
return (
<div role="status" aria-live="polite" className="visually-hidden">
{message}
</div>
);
}Pattern 2: Wait Until the User Stops Typing
Announce once the user pauses, not on every keystroke. Our role="alert" version updated a message after each letter, and VoiceOver spoke "4 results", "2 results", "e", "1 results", "n" while we typed "green": counts that were already out of date, mixed in with the user's own typing. W3C lists "Using role="alert" or aria-live="assertive" on content which is not important and time-sensitive" as a failure of 4.1.3 (W3C, Understanding SC 4.1.3). (It also said "1 results". Handle the singular.)
Scott O'Hara suggests that "a live region could remain empty until a long enough delay since the last key press has been detected" (Scott O'Hara, 2022). GOV.UK's accessible autocomplete, one of the most widely used open-source examples, waits 1,400 milliseconds before updating its status message (statusDebounceMillis = 1400 in its source). Somewhere between one and one and a half seconds is a reasonable starting point; test it with real users if you can.
const statusEl = document.getElementById('search-status');
let statusTimer;
let bump = false;
function announce(message, delay = 1000) {
clearTimeout(statusTimer);
statusTimer = setTimeout(() => {
// Alternate a trailing no-break space so a repeated message still counts as a change
bump = !bump;
statusEl.textContent = bump ? message : `${message} `;
}, delay);
}
function resultsMessage(count, query) {
if (count === 0) return `No results for ${query}`;
return `${count} ${count === 1 ? 'result' : 'results'} for ${query}`;
}
searchInput.addEventListener('input', async (event) => {
const query = event.target.value.trim();
if (!query) return announce('', 0);
const items = await fetchResults(query);
// Ignore a slower, older request, or one that finishes after focus moved to the results
if (query !== searchInput.value.trim() || document.activeElement !== searchInput) return;
renderResults(items);
announce(resultsMessage(items.length, query));
});The no-break space solves a quieter problem. If the same message is set twice in a row, for example when ticking a filter leaves the count unchanged, the region's text doesn't change, so nothing is announced. WordPress hit this years ago and its @wordpress/a11y package still works around it: "Safari + VoiceOver don't announce repeated, identical strings. We use a no-break space to force them to think identical strings are different" (Gutenberg source). GOV.UK's autocomplete solves the same problem by alternating between two status regions. Either works. Including the query in the message, as above, also makes most repeats different anyway.
When the user presses Enter or the Search button, they've asked to go to the results, so move focus instead of announcing. Cancel any pending status message first, so it doesn't talk over the heading:
const resultsHeading = document.getElementById('results-heading');
document.getElementById('search').addEventListener('submit', async (event) => {
event.preventDefault();
const query = searchInput.value.trim();
if (!query) return;
const items = await fetchResults(query);
renderResults(items);
clearTimeout(statusTimer); // after the await, so a typing announcement queued meanwhile is dropped too
resultsHeading.textContent = resultsMessage(items.length, query);
resultsHeading.focus();
});In our test, this was announced as "heading level 2, 2 results for gr": the user hears the count and lands at the top of the results, ready to read down.
Pattern 3: Loading States and aria-busy
Only announce loading if it takes long enough to notice. "Searching..." is a valid status message under 4.1.3, but for a fetch that returns in 300 milliseconds it's just noise before the count. If your search can be slow, start a timer when the request goes out and announce "Searching" only if the results haven't arrived within a second or two.
Don't rely on aria-busy to hold announcements back. The ARIA spec says only that assistive technologies "MAY want to wait" while an element is busy (W3C, WAI-ARIA 1.2), and in practice support is patchy. Sara Soueidan reports that "support for the aria-relevant, aria-atomic, and aria-busy attributes is currently inconsistent across browsers and screen reader pairings", and that "most screen readers (except JAWS) will read the contents of the busy region even before it's done loading" (Sara Soueidan, 2024). Setting it does no harm, but control what's spoken by controlling the status text, not with aria-busy.
Pattern 4: No Results and Errors
Say clearly when nothing matched. "No results returned" is one of W3C's own status message examples. Some experts go further and recommend an assertive message for this one case, because a user who keeps typing may otherwise miss it: Scott O'Hara says "Do ensure that an assertive live region is used to immediately inform a user that no results were found" (Scott O'Hara, 2022), and Sara Soueidan makes the same recommendation.
We'd treat that as a judgement call. A polite message, sent after the typing pause from Pattern 2, is usually enough, and it avoids interrupting someone mid-word. If your search has a lot of zero-result queries, try both with a screen reader user. Whichever you choose, include the query ("No results for oolong") and give the user somewhere to go next on the page itself, such as a link to browse all items.
A failed request is different from an empty one. "Search isn't working right now, try again" is an error that the user needs to act on, and that's the case role="alert" exists for.
Pattern 5: "Load More" Without Losing Focus
Move focus to the first new item, and never remove the element that has focus. In our test, the "load more" button that replaced itself with "That is all of them" after the last page sent focus to the document, and VoiceOver announced the page title followed by "web content". A keyboard user pressing Tab from there starts again at the top of the page.
loadMoreButton.addEventListener('click', async () => {
const firstNewIndex = list.children.length;
const items = await fetchNextPage();
if (items.length === 0) {
// Nothing new: put focus on the last item rather than lose it with the button
list.lastElementChild?.querySelector('a')?.focus();
loadMoreButton.remove();
return;
}
list.insertAdjacentHTML('beforeend', items.map(renderItem).join(''));
// Move focus before the button can disappear
list.children[firstNewIndex].querySelector('a').focus();
if (!hasMorePages()) loadMoreButton.remove();
});Is moving focus to the new item always right? Not everyone agrees. On a W3C mailing list thread, one tester pointed out that leaving focus on the button means a user "has to back through the loaded list which could be long", while another said "It would be so much easier if the focus remained on the button so I could operate it repeatedly" (W3C WAI-IG, 2020). Both are reasonable. Moving focus to the first new item is the safer default, because it puts the user where the content is and it never strands them when the button goes away. If you keep focus on the button instead, then a short status message ("10 more teas loaded") is the right way to say something happened.
Infinite scroll is harder still, because there's no button to press. The APG feed pattern describes how to build one, with Page Down and Page Up moving between articles and Control + End jumping to "the first focusable element after the feed" (W3C APG, Feed pattern). For most search results and product lists, a "load more" button is simpler and easier to get right.
What About ariaNotify()?
document.ariaNotify() is a new browser API that "queues a string of text to be announced by a screen reader" without any live region at all (MDN, 2026). It shipped in Chrome 141 in September 2025 (Chrome for Developers), Firefox 150 in April 2026 (MDN, Firefox 150) and Safari 27 in September 2026 (Apple, Safari 27 release notes), and MDN now marks it as Baseline 2026.
It should avoid the timing problem this whole post is about, because there's no region to register first. (We didn't test it here.) Use it as an enhancement, with a live region as the fallback:
function announceNow(message) {
if (typeof document.ariaNotify === 'function') {
document.ariaNotify(message);
} else {
announce(message, 0); // the live region version from Pattern 2
}
}There are reasons not to drop live regions yet. Older browsers are still in use. MDN notes that live region announcements "take priority over ariaNotify() announcements", and that "Normally, only the most recent announcement is spoken". MDN's compatibility data also notes that on macOS an announcement may not be spoken if another accessibility event fires at the same time, the same conflict we saw between focus and a status message. W3C added ariaNotify as a 4.1.3 technique this year but, as of April 2026, still called it "an experimental feature in web browsers and assistive technologies" to be used "as a progressive enhancement" (W3C, ARIA27). That advice predates Safari's support, but the fallback costs one if statement, so keep it.
Which Screen Readers Should You Test With?
Test with at least two, and include one that Windows users rely on. In WebAIM's Screen Reader User Survey #11, 55.0% of respondents named JAWS as their primary desktop screen reader, 32.9% NVDA and 6.5% VoiceOver (WebAIM, 2026). On mobile, VoiceOver dominates: 72.2% of respondents said they commonly use it, and 29.5% TalkBack.
Live regions behave differently across those pairings, which is why one screen reader isn't enough. Adrian Roselli's tests, run on screen reader versions from 2023 to 2026, found JAWS with Chrome treating "All live regions" as polite (in both its 2023 and 2026 versions), NVDA prefixing role="alert" messages with the word "alert", and Orca not announcing role="alert" at all. TalkBack changed between versions: 13.1 treated every live region as assertive, and 16 treated every one as polite (Adrian Roselli, 2026). Our VoiceOver results are a starting point. NVDA is free, so there's no reason not to run the same checks in it on Windows.
How Do You Test Dynamic Announcements?
Scan first, then test what the scan can't hear. Five checks cover most failures:
- Listen once. Turn on a screen reader (VoiceOver is Cmd+F5 on a Mac; NVDA is a free download on Windows), type a query, and wait. Do you hear a count? Does it come after you stop typing, not during?
- Repeat a message. Tick a filter that leaves the count the same, or search for the same thing twice. Is the count announced the second time?
- Search with no results. Is "No results" announced, with the query?
- Load to the end. Press "load more" with the keyboard until there's nothing left. Where is focus? Press Tab: does it go to the next new item, or back to the top of the page?
- Submit. Press Enter in the search box. If focus moves, does it land on a heading that says how many results there are?
You can automate the two things a scan misses most often: that the status text is set, and that focus survives "load more". These Playwright tests fail for the no-announcement and focus-losing versions in our test:
import { test, expect } from '@playwright/test';
test('search announces a result count', async ({ page }) => {
await page.goto('/teas');
await page.getByRole('searchbox', { name: 'Search teas' }).fill('gr');
// Retries until the debounced message arrives; the regex allows the no-break space
await expect(page.getByRole('status')).toHaveText(/^2 results for gr/);
});
test('load more moves focus to the first new item', async ({ page }) => {
await page.goto('/teas');
await page.getByRole('button', { name: 'Load more teas' }).press('Enter');
// Green tea is the first item on page two; this fails if focus falls to the page
await expect(page.getByRole('link', { name: 'Green tea', exact: true })).toBeFocused();
});The first test checks that the region has the right text, not that a screen reader speaks it; the region-mounted-with-its-message version would pass the first check and still be silent. So keep the manual listen in your release checklist, and run the automated checks with your axe scans in CI, as in our GitHub Actions setup guide.
Frequently Asked Questions
Should I use aria-live or role="status" for search results?
Use role="status", with aria-live="polite" added for compatibility. role="status" already implies polite announcements and aria-atomic="true", so the whole message is read, not just the changed part (W3C, WAI-ARIA 1.2). Put the region in the page from the start and change only its text. In our VoiceOver test, a pre-existing status region was spoken every time.
Why doesn't my live region announce anything?
The most common reason is that the region was added to the page at the same time as its message. Screen readers only announce changes to regions they already know about, so the region must exist before the text arrives (MDN, 2026). Other causes are hiding it with display: none, and setting the same text twice in a row. Our ARIA mistakes guide covers the mounting problem in more detail.
Should focus move to the search results?
Only when the user asked for them, by pressing Enter or a Search button. Then move focus to a heading that states the count. While the user is typing or changing filters, keep focus where it is and announce the count instead; moving focus mid-task can fail WCAG 2.2 SC 3.2.2 On Input. For other kinds of focus movement, see our guide to focus management in single-page apps.
How long should I wait before announcing search results?
About one to one and a half seconds after the last keystroke. GOV.UK's accessible autocomplete waits 1,400 milliseconds before updating its status message (alphagov/accessible-autocomplete). Announcing on every keystroke reads out counts that are already out of date and talks over the user's typing.
Can automated accessibility tools detect missing status messages?
No. axe-core has no rule for WCAG 2.2 SC 4.1.3, and in our test it reported zero issues for all nine versions, including those that VoiceOver never announced. A unit or end-to-end test can check that a status region's text changes, but only a screen reader shows whether it's spoken.
The Bottom Line
The fix for silent search results is small: one empty role="status" region in the page from the start, a short count written into it after the user pauses, and focus moved only when the user asked to go somewhere. What makes it easy to get wrong is that every broken version looks fine to a scanner. In our test, nine versions passed axe and three of them told a VoiceOver user nothing useful. Listen to your search once with a screen reader before you ship it, and add dynamic results to your WCAG 2.2 compliance checklist review.
Sources
- W3C, Web Content Accessibility Guidelines (WCAG) 2.2, retrieved 2026-10-24, https://www.w3.org/TR/WCAG22/
- W3C, Understanding SC 4.1.3 Status Messages, retrieved 2026-10-24, https://www.w3.org/WAI/WCAG22/Understanding/status-messages.html
- W3C, Technique ARIA27: Using ariaNotify to convey the status of a progress bar, retrieved 2026-10-24, https://www.w3.org/WAI/WCAG22/Techniques/aria/ARIA27
- W3C, Accessible Rich Internet Applications (WAI-ARIA) 1.2, retrieved 2026-10-24, https://www.w3.org/TR/wai-aria-1.2/
- W3C WAI-ARIA Authoring Practices Guide, Feed pattern, retrieved 2026-10-24, https://www.w3.org/WAI/ARIA/apg/patterns/feed/
- W3C WAI Interest Group mailing list, "Load more" focus thread, August 2020, retrieved 2026-10-24, https://lists.w3.org/Archives/Public/w3c-wai-ig/2020JulSep/0119.html
- MDN Web Docs, ARIA live regions, retrieved 2026-10-24, https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Guides/Live_regions
- MDN Web Docs, Document: ariaNotify() method, retrieved 2026-10-24, https://developer.mozilla.org/en-US/docs/Web/API/Document/ariaNotify
- MDN Web Docs, Firefox 150 release notes for developers, retrieved 2026-10-24, https://developer.mozilla.org/en-US/docs/Mozilla/Firefox/Releases/150
- Chrome for Developers, Chrome 141 release notes, retrieved 2026-10-24, https://developer.chrome.com/release-notes/141
- Apple, Safari 27 release notes, retrieved 2026-10-24, https://developer.apple.com/documentation/safari-release-notes/safari-27-release-notes
- Deque, axe-core rule descriptions, retrieved 2026-10-24, https://github.com/dequelabs/axe-core/blob/develop/doc/rule-descriptions.md
- WebAIM, Screen Reader User Survey #11 Results, retrieved 2026-10-24, https://webaim.org/projects/screenreadersurvey11/
- GOV.UK Technology blog, Improving accessibility on GOV.UK search, retrieved 2026-10-24, https://technology.blog.gov.uk/2014/08/14/improving-accessibility-on-gov-uk-search/
- alphagov, accessible-autocomplete status.js, retrieved 2026-10-24, https://github.com/alphagov/accessible-autocomplete/blob/main/src/status.js
- WordPress Gutenberg, @wordpress/a11y filter-message.ts, retrieved 2026-10-24, https://raw.githubusercontent.com/WordPress/gutenberg/trunk/packages/a11y/src/shared/filter-message.ts
- Adrian Roselli, Live Region Support, retrieved 2026-10-24, https://adrianroselli.com/2026/01/live-region-support.html
- Sara Soueidan, Accessible notifications with ARIA Live Regions (Part 1), retrieved 2026-10-24, https://www.sarasoueidan.com/blog/accessible-notifications-with-aria-live-regions-part-1/
- Sara Soueidan, Accessible notifications with ARIA Live Regions (Part 2), retrieved 2026-10-24, https://www.sarasoueidan.com/blog/accessible-notifications-with-aria-live-regions-part-2/
- Scott O'Hara, Are we live?, retrieved 2026-10-24, https://www.scottohara.me/blog/2022/02/05/are-we-live.html
- Scott O'Hara, Dynamic results, retrieved 2026-10-24, https://www.scottohara.me/blog/2022/02/05/dynamic-results.html
- Andrea de Souza, HTMHell Advent Calendar 2023, day 22, retrieved 2026-10-24, https://htmhell.dev/adventcalendar/2023/22/
- Guidepup, screen reader automation, retrieved 2026-10-24, https://www.guidepup.dev/
- Wikimedia Commons, White noise: Horn loudspeakers at Brastad soccer arena (CC0), cover image, https://commons.wikimedia.org/wiki/File:White_noise_-_Horn_loudspeakers_at_Brastad_soccer_arena.jpg