# Auto-updating feeds that change while people read them

Source: https://easeweb.dev/learn/auto-updating-feeds
Topics: Keyboard and focus > The page changes only when the user asks (WCAG 3.2.5)
The fix: Change the list only when people ask. Either keep fetching, but write the count into a "Show 1 new story" button before the list and add the new stories when it is pressed, or put a checkbox before the list that turns the automatic updates off, and remember the choice.
Test with: keyboard, screen reader, zoom
References: https://www.w3.org/WAI/WCAG22/Understanding/change-on-request.html https://www.w3.org/WAI/WCAG22/Techniques/failures/F61 https://www.w3.org/WAI/WCAG22/Techniques/general/G76 https://www.w3.org/WAI/WCAG22/Understanding/on-focus.html https://www.w3.org/WAI/WCAG22/Understanding/pause-stop-hide.html https://www.w3.org/WAI/WCAG22/Techniques/failures/F41 https://amp.dev/documentation/components/amp-live-list

This is a recreated teaching example, not a finding about a named client. The code is a starting pattern: test it in your own product. No completed assistive-technology test is claimed here.

## What happens

A local news site, the Harbour Herald, has a Latest page: a numbered list of the day's stories, newest first, each with the time it was posted. So that the page never shows old news, a script fetches the list again every 60 seconds and writes it over the one on the page. For someone glancing at the whole list, new stories simply appear at the top.

Someone using a screen reader tabs down the headlines and stops on the third, "Library extends its weekend opening hours", to decide whether to open it. A minute after the page loaded, the list is written over. The link they were on is taken out of the page and a new copy put in its place, so focus falls back to the page itself and nothing says what happened. It happens every minute, even when no story has come in, because the links are new elements each time. At 10:43 a story about ferries comes in, and the library story moves down a place. Someone zoomed in on it with a magnifier sees it drop out of view and the bridge story take its place. Nothing on the page turns the updates off.

## Why it fails

Success Criterion 3.2.5 Change on Request (Level AAA) asks that changes of context happen only when people ask for them, or that people can turn such changes off. WCAG's changes of context include moving focus somewhere else and significantly re-arranging the content of a page, because they can disorient people who can't see the whole page at once. Writing over the list does both. The main content is replaced by itself, and the focused link goes with it: when the element that has focus is removed, the browser moves focus to the page itself, and `document.activeElement` then returns the body. W3C names this failure F61, the content in the main viewport changed by an automatic update that people can't turn off, and its first example is a news site that refreshes itself to show the newest headlines. The sufficient technique for automatic updates, G76, gives people a way to ask for the update instead.

The browser has no reason to stop it. A timer runs whether or not anyone is reading, and `innerHTML` throws away every element it replaces, the focused one included, even when the new markup is the same.

The same list also falls under 2.2.2 Pause, Stop, Hide at Level A, since it updates by itself beside the rest of the page, and 2.2.2 asks for a way to pause, stop or hide such updates or to set how often they come. Both fixes below meet it too. A page that reloads itself on a timer, rather than rewriting part of itself, fails 2.2.1 first: [pages that reload or redirect themselves on a timer](/learn/meta-refresh) covers that.

## Who is affected

The update is made for people who look at the whole list and take in what is new. It costs everyone who moves through the list one story at a time, and the more slowly they move, the more often it catches them.

## What automation and AI miss

There is nothing wrong in the markup, and a scan loads the page and leaves within seconds, long before the first update. The audit watches the timers a page starts by itself and sends one that fetches content and writes it into the page as markup to review, as `ew:auto-refresh-manual-check`: only a person can see whether it replaces the main content, and whether a switch before it turns it off. A meta refresh that reloads the page is axe's `meta-refresh` when the delay is under 20 hours; a longer one passes 2.2.1, and the audit sends it to review as `axe:meta-refresh-no-exceptions`. Asked to "keep the list up to date", AI coding tools can write the `setInterval` in the Before.

## Before: a list that writes over itself every minute

```html
<section aria-labelledby="latest-title">
  <h2 id="latest-title">Latest</h2>
  <ol id="stories">
    <li>
      <time datetime="10:40">10:40</time>
      <a href="/news/storm-warning">Storm warning for the coast tonight</a>
    </li>
    …
  </ol>
</section>
```

```js
const list = document.getElementById('stories');

setInterval(async () => {
  const response = await fetch('/latest');
  list.innerHTML = await response.text();
}, 60000);
```

- Every 60 seconds, the whole list is replaced, whether or not anyone is reading it.
- The focused story's link is removed with it, so focus falls back to the page.
- When a story comes in, every story below it moves down one place.
- Nothing turns the updates off.

## After: a button that shows the new stories

```html
<section aria-labelledby="latest-title">
  <h2 id="latest-title">Latest</h2>
  <button type="button" id="show-new" hidden>Show 1 new story</button>
  <ol id="stories">
    <li>
      <time datetime="10:40">10:40</time>
      <a href="/news/storm-warning">Storm warning for the coast tonight</a>
    </li>
    …
  </ol>
</section>
```

```js
const list = document.getElementById('stories');
const showNew = document.getElementById('show-new');
let waiting = '';

setInterval(async () => {
  const newest = list.querySelector('time').dateTime;
  const response = await fetch(`/latest?after=${newest}`);
  const { count, html } = await response.json();
  if (!count) return;
  waiting = html;
  showNew.textContent = count === 1 ? 'Show 1 new story' : `Show ${count} new stories`;
  showNew.hidden = false;
}, 60000);

showNew.addEventListener('click', () => {
  list.insertAdjacentHTML('afterbegin', waiting);
  waiting = '';
  showNew.hidden = true;
  list.querySelector('a').focus();
});
```

1. Keep fetching every 60 seconds, but ask only for the stories newer than the first one on the page, and keep them aside instead of writing them into the list.
2. Write how many there are into a button before the list, and show it. The list doesn't change, so nobody loses their place.
3. When the button is pressed, add the new stories to the top of the list. This is the change people asked for.
4. Move focus to the first new story, since the button they pressed is hidden again. They are now on what they asked to see.

This fixes the Before and passes 3.2.5: the main content changes only when someone presses the button. It also meets 2.2.2, since the list no longer changes by itself.

## After: a checkbox that turns the updates off

```html
<section aria-labelledby="latest-title">
  <h2 id="latest-title">Latest</h2>
  <input type="checkbox" id="auto-update" checked>
  <label for="auto-update">Update this list automatically</label>
  <ol id="stories">
    <li>
      <time datetime="10:40">10:40</time>
      <a href="/news/storm-warning">Storm warning for the coast tonight</a>
    </li>
    …
  </ol>
</section>
```

```js
const list = document.getElementById('stories');
const autoUpdate = document.getElementById('auto-update');
autoUpdate.checked = localStorage.getItem('auto-update') !== 'off';
autoUpdate.addEventListener('change', () => {
  localStorage.setItem('auto-update', autoUpdate.checked ? 'on' : 'off');
});

setInterval(async () => {
  if (!autoUpdate.checked) return;
  const response = await fetch('/latest');
  list.innerHTML = await response.text();
}, 60000);
```

1. Put a checkbox, "Update this list automatically", before the list, so people reach it before they reach the stories.
2. Check it at the start of each update, and skip the update while it is unticked. The list then stays as it is for as long as people want.
3. Save the choice and read it back when the page loads, so someone who turned the updates off doesn't meet them again on the next visit.

This fixes the Before and passes 3.2.5, which allows a way to turn the changes off, and 2.2.2. It leaves the updates on until someone turns them off: a screen reader user who starts on the stories, or arrives from a link straight into the list, still loses their place before they find the checkbox.

## Which option to use

Both pass 3.2.5. The button is recommended: nothing changes until someone asks, so nobody has to find a setting first, while the checkbox is not enough on its own, because the updates still reach everyone who hasn't yet found it. The button costs one press for people who want the news as it comes; the checkbox suits a page whose readers mostly want it to keep up by itself, such as a live score, as long as the checkbox comes first and the choice is remembered.

## Implementation decisions

**Where focus goes.** After the button is pressed it is hidden, and a hidden element can't keep focus, so the script moves focus to the first new story. Don't leave it for the browser to drop on the page. If the design keeps the button visible, focus can stay on it, with the count changed to "No new stories".

**Saying that new stories are waiting.** The button shows up silently, and that is enough for 3.2.5: people find it when they come back to the top of the list. If screen reader users should hear of it, write a short message into a `role="status"` region when the count goes from none to one, not on every check, or a message every minute becomes the next interruption. [Countdown timers that screen readers keep reading out](/learn/countdown-timers) shows why.

**A floating "new stories" pill.** Some sites draw the button as a pill fixed to the top of the screen. Keep it before the list in the markup, so it comes before the stories in reading and Tab order, and make sure it never covers the story that has focus, as [sticky headers and banners that hide keyboard focus](/learn/focus-not-obscured) describes.

**Pausing while someone is on the list is not a fix.** A script that holds the updates while focus or the pointer is inside the list still changes it the moment they leave, and a screen reader reading the list doesn't always move focus as it reads, so the script can't tell anyone is there.

**Adding only the new stories at the top.** Inserting the new items with `insertAdjacentHTML('afterbegin', …)` instead of rewriting the list keeps the focused link in the page, which is better than the Before. It still moves every story down under a magnifier while someone reads, so give people the button or the checkbox as well.

**Pages that reload themselves.** A Latest page with `<meta http-equiv="refresh" content="300">` is the same failure by another route, and fails 2.2.1 as well. Delete the meta refresh, whatever its delay, and give people a button or a link that loads the new stories when they ask.

**Live pages where the updates are the point.** A live blog, a match page or a departures board exists to keep up, but 3.2.5 makes no exception for them. Give them the same button or checkbox; the people following a match closely will press it or leave the updates on.

## Verify the fix

1. Search the scripts for `setInterval` and `setTimeout` callbacks that call `fetch` or `XMLHttpRequest` and write markup with `innerHTML`, `insertAdjacentHTML` or `replaceChildren`. Each one either writes only into a button, or first checks a checkbox before the list.
2. Put the mouse aside, Tab to a story in the middle of the list, and wait for longer than the update interval. Focus stays on that story, and the story stays where it was.
3. When new stories are waiting, press the button: they appear at the top and focus moves to the first one. With the checkbox instead, untick it, wait again, reload the page, and check that it is still unticked and the list still stays put.
4. With a screen reader, read the list item by item for two minutes: your place is never lost, and the button or checkbox says what it does.
5. At 400% zoom, keep one story in view for longer than the update interval: it doesn't move until you ask for new stories.

Record the OS, browser and assistive-technology versions, the build, the date, and the actual result of each step. Repeat after the feed component or its update script changes.

## Limits

This is a starting pattern, not a guarantee for every application. It covers a list that replaces itself on a timer. The other changes 3.2.5 asks people to start, such as a window that opens when a page loads or when text is typed, fail in the same way and take the same fix: let people start them. A field or select that changes the page when it is focused or changed is covered in [dropdowns and fields that change the page on their own](/learn/context-changes), and a link that opens a new tab in [links that open a new tab without saying so](/learn/new-tab-links). Screen readers differ in what they do when the element they were reading is removed: test with the ones your users have.

---

Get this guide in your coding agent: MCP https://easeweb.dev/mcp (setup at https://easeweb.dev/services/developers)
