Accessible Live Log Viewers

Permalink to "Accessible Live Log Viewers"

A live log viewer is a panel that streams lines of text as a process runs: CI build output, a deployment, an import job, an audit trail. It seems a natural fit for role="log" — the ARIA role literally named for it — but a naive implementation has every line announced, auto-scrolls the panel so keyboard users cannot read what has passed, and grows the DOM without limit until the page stalls.

This page builds a viewer that is quiet by default, loud for errors, readable by keyboard at any point in the stream, and bounded in size. It belongs to real-time data stream announcements.

Spec reference

Permalink to "Spec reference"

role="log" (ARIA 1.2) is a live region “where new information is added in meaningful order and old information may disappear”. Its implicit properties are aria-live="polite", aria-atomic="false" and aria-relevant="additions": appended lines are announced one by one, and nothing else. The role needs an accessible name.

Because the implicit politeness is polite, a busy log speaks continuously. For high-volume logs, set aria-live="off" on the log to silence line-by-line announcements, and route selected events (errors, completion) through the page’s status and alert regions instead. The log keeps its role — it is still a log — but no longer floods speech.

Criteria: SC 4.1.3 Status Messages (errors and completion), SC 2.2.2 Pause, Stop, Hide (auto-updating, auto-scrolling content must be pausable), SC 2.1.1 Keyboard (history must be readable by keyboard), SC 1.4.1 Use of Color (severity not by colour alone).

A log viewer's controls and stream Mock of a log viewer: a control row with follow-tail, severity filter and error navigation, and log lines with severity labels. A log viewer's controls and streamTimeLevelMessage10:02:112INFOInstalling dependencies10:02:40WARNPeer dependency missing10:03:05ERROR1Test "sort" failed10:03:06INFO1 of 212 tests failed1ERROR is text in the line and is announcedthrough the alert region2Follow tail toggle scrolls to new lines butleaves focus where the user put it3Next error and previous error buttonsmove focus to the line
Severity is a word, not just a colour — and following the tail never moves focus.

When to announce lines — and when not to

Permalink to "When to announce lines — and when not to"

Announce individual lines only for low-volume logs where every line matters: a deployment with a dozen steps, an approval audit trail. The role="log" defaults then work as intended.

For high-volume logs — build output, verbose job logs — silence the line stream and announce milestones: errors, warnings (optionally), step changes and completion. “Build failed: 1 of 212 tests failed” is what the user needs; the 4,000 lines before it are for reading on demand.

The misapplication to name is auto-scrolling the log by moving focus to the newest line. It yanks keyboard users away from whatever they were reading every time a line arrives.

Annotated code example

Permalink to "Annotated code example"
<section aria-labelledby="log-h" class="log-viewer">
  <h2 id="log-h">Build #4812 output</h2>
  <div class="log-controls" role="group" aria-label="Log controls">
    <!-- SC 2.2.2: auto-scroll is a user choice -->
    <button type="button" id="follow" aria-pressed="true">Follow new lines</button>
    <label for="level">Show</label>
    <select id="level"><option>All</option><option>Warnings and errors</option><option>Errors</option></select>
    <button type="button" id="prev-err">Previous error</button>
    <button type="button" id="next-err">Next error</button>
  </div>
  <!-- role=log for structure; aria-live off for high volume -->
  <ol role="log" aria-label="Build output" aria-live="off" id="log" tabindex="0"></ol>
</section>
const MAX_LINES = 2000;                   // bound the DOM (and the accessibility tree)
const log = document.getElementById('log');
let follow = true;

function appendLine({ time, level, text }) {
  const li = document.createElement('li');
  li.dataset.level = level;
  li.tabIndex = -1;                       // focusable for error navigation
  // SC 1.4.1: level as text, styled by data attribute
  li.innerHTML = `<time>${time}</time> <span class="lvl">${level}</span> <span class="msg"></span>`;
  li.querySelector('.msg').textContent = text;
  log.append(li);
  if (log.children.length > MAX_LINES) log.firstElementChild.remove();

  // Scroll the container, never move focus (SC 2.4.3)
  if (follow) log.scrollTop = log.scrollHeight;

  // SC 4.1.3: milestones only
  if (level === 'ERROR') alertRegion(`Error: ${text}`);
}

function onComplete({ ok, summary }) {
  status(`Build ${ok ? 'passed' : 'failed'}. ${summary}`);   // e.g. "1 of 212 tests failed."
}

// Scrolling up to read stops following automatically; the toggle reflects it
log.addEventListener('scroll', () => {
  const atBottom = log.scrollHeight - log.scrollTop - log.clientHeight < 8;
  if (!atBottom && follow) setFollow(false);
});
function setFollow(on) { follow = on; document.getElementById('follow').setAttribute('aria-pressed', String(on)); }

// Error navigation: focus the line so screen readers read it
document.getElementById('next-err').addEventListener('click', () => {
  const errs = [...log.querySelectorAll('li[data-level="ERROR"]')];
  const cur = errs.findIndex((e) => e === document.activeElement);
  (errs[cur + 1] ?? errs[0])?.focus();
});

Stopping follow mode when the user scrolls up is the behaviour sighted users know from terminals and CI tools. Doing it automatically — and reflecting it in the toggle’s pressed state — means keyboard users who move back through history are not dragged to the bottom by the next line.

Keyboard & AT behaviour

Permalink to "Keyboard & AT behaviour"
Event Expected behaviour Announcement
Line appended (INFO) Line added; container scrolls if following None (aria-live="off")
Line appended (ERROR) Same Assertive: “Error: Test “sort” failed”
Build finishes — “Build failed. 1 of 212 tests failed.”
“Next error” Focus on the error line “10:03:05 ERROR Test “sort” failed”
Arrow keys in the log (browse mode) Read line by line Lines as list items, with position
Scroll up in the log Follow turns off “Follow new lines, toggle button, not pressed” (on focus)
A naive log and a considered one Comparison of a log viewer that announces every line and moves focus to the newest line against one that announces milestones and scrolls without moving focus. A naive log and a considered one✗ Naive logEvery line announced politelyFocus moved to each new lineSeverity shown by colour onlyUnlimited lines in the DOM✓ Considered logaria-live off; errors and completion announcedContainer scrolls; focus untouchedSeverity as a word in each lineLine cap, with the full log downloadable
The role is the same; the volume, the scrolling and the navigation are what make it usable.

Integration context

Permalink to "Integration context"

The role’s defaults and when to override them are covered in role status, alert and log compared. For logs where lines should be announced but arrive in bursts, pace them with throttling high-frequency aria-live updates.

The line cap exists for the reasons in measuring accessibility tree cost for large tables: a 50,000-line log in the DOM slows every screen reader on the page. For logs that must be fully navigable in place, virtualize with position metadata as in making TanStack Virtual lists and tables accessible.

A build log, heard Timeline of a build log from the user's perspective: silence during routine lines, an error announced immediately, the user jumping to it, and the completion summary. A build log, heardBuild starts"Build #4812 started."Routine linessilent; table of lines growsError"Error: Test sort failed"Next errorfocus to the failing lineComplete"Build failed. 1 of 212 testsfailed."one build, as heard by a screen reader user
Thousands of lines, three announcements — the ones that matter.

Gotchas

Permalink to "Gotchas"

Colour-coded levels. Red for errors and yellow for warnings are invisible to many users and to screen readers. Always include the level as text.

ANSI escape codes. Build logs contain colour codes; strip them before rendering, or screen readers read “escape bracket 31 m”.

Removing old lines while focused. If the line cap removes the focused line, move focus to the log container first.

Design system notes

Permalink to "Design system notes"

A LogViewer component should default to aria-live="off" for streams, own the follow toggle and its auto-disable, render levels as text, cap lines with a download link, and expose onMilestone hooks that route to the shared announcer. Teams streaming low-volume audit logs can opt into per-line announcements explicitly.

Testing checklist

Permalink to "Testing checklist"

FAQ

Permalink to "FAQ"
Should a live log viewer use role="log"?

Yes — it gives the region the right structure and defaults. For high-volume logs, set aria-live=“off” on it so lines are not all announced, and announce errors and completion through the page’s status and alert regions.

How should auto-scrolling work in an accessible log?

Scroll the log container to new lines only while “follow” is on, never move keyboard focus, and turn follow off automatically when the user scrolls up to read, reflecting that in the toggle’s pressed state.

How can screen reader users find errors in a long log?

Provide Next error and Previous error controls that move focus to error lines, and a severity filter. Write the severity as text in each line so it is read with the message.

How many lines should be kept in the page?

A bounded number — a few thousand at most — so the accessibility tree stays manageable. Offer the complete log as a download or through a virtualized view.

Permalink to "Related"

← Back to Real-Time Data Stream Announcements