Last reviewed: July 30, 2026

Direct answer

Make CometAPI chat updates work with screen readers by separating the conversation, routine status, and urgent error into stable regions with different jobs. Put the ordered conversation inside a named, neutral role='log' wrapper that contains the native ordered list. Put short progress and completion messages in a persistent role='status' region. Use aria-busy while a meaningful update is incomplete. Reserve a persistent role='alert' region for failures that require immediate attention. Do not move keyboard focus merely because the request state changed.

This structure follows the W3C explanation of WCAG 2.2 status messages , which says assistive technology should be able to present status changes without those messages receiving focus. The MDN status role reference documents that status is a polite, atomic live region. The MDN log role reference identifies chat history as an ordered live-region use case and requires the log to have an accessible name.

Start with the live-region elements already present in the document. Updating an existing region is more dependable than creating a populated alert only after a failure occurs. Keep role='log' on a neutral wrapper so the inner ol retains its native list semantics.

<section class='chat' aria-labelledby='conversation-title'>
  <h2 id='conversation-title'>Conversation</h2>

  <div
    id='conversation-log'
    role='log'
    aria-labelledby='conversation-title'
    aria-relevant='additions text'
    aria-busy='false'
  >
    <ol id='conversation-list'></ol>
  </div>

  <p id='chat-status' role='status'></p>
  <p id='chat-error' role='alert'></p>

  <form id='chat-form'>
    <label for='chat-message'>Message</label>
    <textarea id='chat-message' name='message' required></textarea>
    <button id='chat-send' type='submit'>Send</button>
  </form>
</section>

Wire those regions to the same state transitions that already control the visible interface. The following browser-side pattern deliberately leaves the network implementation behind requestReply. It does not assume a CometAPI endpoint, payload, or response field that is not covered by this article’s source pack.

The send button remains focusable while a request is pending. aria-disabled exposes its pending state, while the requestPending guard prevents repeat submissions. This avoids relying on native disabled for a control that may still hold focus.

const logRegion = document.querySelector('#conversation-log');
const messageList = document.querySelector('#conversation-list');
const statusRegion = document.querySelector('#chat-status');
const errorRegion = document.querySelector('#chat-error');
const form = document.querySelector('#chat-form');
const input = document.querySelector('#chat-message');
const sendButton = document.querySelector('#chat-send');

let requestPending = false;

function setPending(isPending) {
  requestPending = isPending;
  logRegion.setAttribute('aria-busy', String(isPending));
  sendButton.setAttribute('aria-disabled', String(isPending));
  statusRegion.textContent = isPending ? 'Waiting for a reply.' : '';
}

function appendMessage(speaker, text) {
  const item = document.createElement('li');
  const label = document.createElement('strong');
  const body = document.createElement('span');

  label.textContent = `${speaker}: `;
  body.textContent = text;
  item.append(label, body);
  messageList.append(item);
}

async function handleChatUpdate(message) {
  if (requestPending) {
    statusRegion.textContent = 'A reply is already in progress.';
    return;
  }

  const startedAt = performance.now();
  errorRegion.textContent = '';
  appendMessage('You', message);
  setPending(true);

  try {
    const reply = await requestReply(message);
    appendMessage('Assistant', reply.text);
    setPending(false);
    statusRegion.textContent = 'Reply added.';

    logClientEvent({
      event_name: 'chat_update',
      outcome: 'success',
      failure_class: null,
      elapsed_ms: Math.round(performance.now() - startedAt),
      response_mode: 'complete',
      message_count: messageList.children.length,
      interface_state: 'idle',
      retryable: false,
    });
  } catch (error) {
    setPending(false);
    statusRegion.textContent = '';
    errorRegion.textContent = 'The reply could not be loaded. Try again.';

    logClientEvent({
      event_name: 'chat_update',
      outcome: 'failure',
      failure_class: classifyFailure(error),
      elapsed_ms: Math.round(performance.now() - startedAt),
      response_mode: 'complete',
      message_count: messageList.children.length,
      interface_state: 'error',
      retryable: isRetryable(error),
    });
  }
}

form.addEventListener('submit', (event) => {
  event.preventDefault();
  const message = input.value.trim();

  if (message) {
    handleChatUpdate(message);
  }
});

The key behavior is the state transition, not the framework. The waiting message is advisory, so it uses status. The completed reply is added to the ordered list inside the log region. An urgent failure updates the existing alert region. Focus stays where the user left it unless the product has a separate, documented reason to move it.

Who this is for

This pattern is for frontend developers maintaining a browser-based CometAPI chat interface, including projects built with plain JavaScript, React, Vue, Svelte, or another DOM-based framework. It is also useful to test engineers and operators who need a repeatable success and failure checklist rather than a visual-only review.

The ARIA markup is specifically for web content. Native applications should implement the same state model through their platform accessibility APIs instead of copying these attributes literally. This article also assumes that the application already has a server-side CometAPI integration. It focuses on how the browser communicates resulting state changes.

Key takeaways

  • Use a named, neutral role='log' wrapper around a native ordered list for messages that arrive in meaningful chronological order.
  • Use role='status' for routine waiting, completion, and cancellation messages that should not interrupt the user.
  • Use aria-busy='true' only while a region’s update is incomplete, and always restore it to false on success, failure, or cancellation.
  • Use role='alert' sparingly for important failures, not for every network transition.
  • Keep live-region nodes mounted before the first update and keep keyboard focus under user control.
  • Keep the initiating control focusable during pending work and guard repeat submissions in application state.
  • Announce meaningful completed units rather than every partial fragment of a streamed reply.
  • Log state and timing metadata, not conversation text or raw network data.
  • Test the happy and error paths with a supported browser and screen reader; markup alone is not a completed accessibility test.

Sources checked

The implementation boundaries in this article come from five refetched public accessibility references:

These sources establish browser accessibility behavior. They do not establish a CometAPI network schema, model response shape, or streaming contract, so this article does not invent those details.

Contract details to verify

Define an explicit interface-state contract before connecting live regions to the request client. At minimum, document what the application renders and announces in each state.

Application stateVisible resultAccessibility mappingFocus behavior
IdleComposer and existing historyEmpty status and alert regionsRemains in the user’s chosen control
SendingOne concise waiting messagestatus, busy log wrapper, and aria-disabled='true' on the focusable submit controlDoes not move
CompletedFinal reply appended to historyNew list item inside log, then busy and disabled states become falseDoes not move
Recoverable failurePlain-language failure and separate retry controlalert only when immediate attention is justifiedDoes not move automatically
CancelledCancellation confirmationstatusReturns only through an explicit product rule

Verify the actual CometAPI client contract separately. Identify whether the browser receives a complete reply or a stream, which application field contains displayable text, how cancellation is represented, and how stale responses are rejected. The guide to reading a CometAPI text response object is a useful companion for that boundary.

For streaming interfaces, do not append every small fragment as a separate log entry. Either update a region marked busy and expose the completed message when the update finishes, or maintain a visual staging area and append one meaningful accessible message at a time. The MDN aria-busy reference supports delaying announcements while multiple changes are incomplete. Exact announcement timing still varies, so test the combinations your product supports.

Happy-path operator workflow

  1. Load the page and confirm the empty log wrapper, status, and alert nodes are already in the accessibility tree. Confirm that the ordered list remains a list inside the log.
  2. Start a supported screen reader, use only the keyboard, enter a short test message, and submit it.
  3. Confirm that the waiting state is announced once and keyboard focus remains in the documented composer control. Activate Send again and confirm that the state guard does not create a second request.
  4. Complete the request through a controlled test adapter. Confirm that one final assistant message is appended to the end of the ordered list inside the named log.
  5. Confirm that aria-busy and aria-disabled return to false, the repeat-submission guard resets, and the completion message is concise.
  6. Inspect the client event. It should contain the event name, outcome, failure class, elapsed time, response mode, message count, interface state, and retryability only.
  7. Repeat with two sequential requests to confirm ordering and ensure that a late first response cannot overwrite a newer result.

Error-path operator workflow

  1. Configure a test adapter to produce a timeout, an unavailable-service response, and a malformed application result in separate runs.
  2. Submit a message and confirm the same single waiting announcement used by the happy path.
  3. Trigger the failure and verify that the busy and disabled states clear, the repeat-submission guard resets, focus remains in its documented control, and existing conversation history remains intact.
  4. Confirm that the failure message is announced once. Keep any retry button outside the alert text because the alert role is intended for text, not interactive controls.
  5. Retry successfully and confirm that the old failure is cleared before the new request begins.
  6. Inspect sanitized logs. Record a broad failure class such as timeout, network, service, validation, or render, but omit the message, reply, raw response, headers, cookies, and account data.

This workflow covers what operators can observe and reproduce. It should run against a controlled test path rather than depending on a live failure.

Failure modes

The log role replaces native list semantics. Applying role='log' directly to an ol replaces the element’s native list role. Put role='log' and aria-busy on a neutral wrapper, then keep the ordered list and its list items inside it.

The live region is mounted too late. A framework conditionally creates an already-populated alert after the request fails. Some browser and assistive-technology combinations may not treat that as an update. Keep an empty alert node mounted, then change its text when the failure occurs. The MDN alert guidance specifically recommends priming the node before updating it.

Routine state changes use alerts. Announcing sending, waiting, every partial update, completion, and failure assertively creates an interruption storm. Use status for ordinary progress and reserve alert for information that needs immediate attention.

Every streamed fragment becomes a log item. Because a log is a live region, appending tiny fragments can produce repetitive announcements and destroy the meaningful reading order. Batch the accessible update into sentences or a completed reply.

The log has no accessible name. MDN states that a log requires an accessible name. Connect the neutral wrapper to a visible heading with aria-labelledby, or provide a concise label when no visible heading exists.

The busy state never clears. A success handler restores aria-busy, but timeout, cancellation, or parsing branches do not. Put cleanup in a shared state transition and test every terminal path. A stuck busy state may suppress the update users need to hear.

The focused submit control is natively disabled. If disabling that control causes focus to move in a supported browser combination, the interface breaks its own focus contract. Keep the control focusable during pending work, expose its pending state with aria-disabled, and guard repeated activation in application state. Otherwise, define and test an explicit focus transition.

Waiting text simply disappears. W3C notes that removing busy text can convey completion visually without giving a non-visual user equivalent information. Replace it with a short completion or cancellation message when the result itself does not make the new state clear.

Focus jumps to the transcript. Moving focus after each reply interrupts typing and changes the user’s context. Keep focus stable for routine updates. Offer an explicit control for moving to the newest message if user research shows that it is needed.

Nested live regions duplicate output. A log contains another status region, or a component adds redundant live settings at multiple levels. Simplify the tree so each state change has one announcement owner, then verify the accessibility tree and spoken result.

A late response wins a retry race. The first request completes after a retry and appends an obsolete reply. Track the active operation in application state, ignore stale completions, and log the broad outcome without storing conversation content.

FAQ

Do I need to add aria-live to role='status'?

Usually no. According to MDN, status already has implicit polite live behavior and implicit atomic behavior. Adding redundant attributes does not replace the need to test, and it can make the intended ownership less clear.

Why use role='log' for the conversation?

A conversation is ordered: new messages are appended after earlier messages and that sequence matters. MDN names chat logs and messaging history as examples for the log role. Give a neutral log wrapper an accessible name, retain the native ordered list inside it, and append new list items at the end.

Should every CometAPI request failure use role='alert'?

No. Use an alert when the message is important and time-sensitive enough to interrupt. A background retry notice or non-urgent reconnect state may fit a polite status. If the user must interact with a dialog, use the appropriate dialog pattern rather than putting controls inside an alert.

How should a streaming reply be announced?

Avoid announcing every fragment. Keep the conversation region busy while assembling a meaningful unit, then expose a sentence or complete message. If the visual interface streams continuously, separate that visual rendering from the accessible final update and test that users receive enough progress information without repetition.

Should focus move to a new assistant reply?

Not by default. WCAG status-message guidance is designed for updates that do not take focus. Preserve the user’s position in the composer and provide an explicit navigation control when moving to the newest reply is useful.

What should client logs contain?

Use bounded operational fields: event name, outcome, broad failure class, elapsed time, response mode, message count, interface state, and retryability. Do not store conversation text or raw network material in routine client telemetry.

Reader next step

Add the three stable regions to a development branch, put the native ordered list inside the neutral log wrapper, connect them to one shared request-state reducer, and run both operator workflows with the browser and screen reader combinations your product supports. Fix invalid role overrides, duplicate announcements, missing completion states, stuck busy flags, repeat-submission gaps, and focus movement before changing visual styling.

Then align the accessible text with the application’s actual presentation. The guide to choosing raw JSON or rendered text helps define what belongs in the visible transcript. For a reproducible timeout branch, follow the companion tutorial on adding client-side timeout handling .