I have had the privilege of helping to improve the accessibility of multiple immersive LLM chat experiences in the course of my career. Now I want to share this knowledge with you.

Accessibility is a core part of a functional digital experience. As the LLM chat pattern continues to spread like wildfire across the web, we risk excluding people if these experiences are not built with accessibility as a core consideration.

It is also worth re-mentioning what “exclusion” means here: It’s the loss of knowledge, the loss of opportunity, the loss of utility, the loss of employability, the loss of entertainment, and more.

What Is An Immersive LLM Chat Experience?

“Immersive LLM chat experience” is a catch-all term for full-screen experiences like Claude, ChatGPT, GitHub Copilot, Gemini, America.gov, Glean, and other similar websites and web apps:

The majority of what this post covers applies to both web apps and native apps. This is because these experiences are typically built using web technologies, then placed in a native wrapper via frameworks such as Electron or Tauri.

Immersive LLM chat experiences are not patterns such as a pop-up chat, inline text generation utilities, or summarization tools. These simpler applications of LLM technology will have different considerations and expectations. Instead, this is a starting point for making those other experiences accessible.

Reframing

There is nothing special about chat-based UIs when viewed via the lens of the Web Content Accessibility Guidelines. They are interactive experiences, built using web technologies. The losses I’ve described — and the legal liability associated with them — are access barriers in the same way any other web content can be an access barrier.

I would also like to mention the social model of disability here: the idea that exclusion can be created by the built environment. For example, consider an elevated entryway that uses a gently sloping ramp in place of stairs.

Built environments also extend to digital experiences, and the social model also applies here. The main activities undertaken to operate an LLM chat are already well-established:

  • Inputting text and submitting it, and
  • Reading content and taking action on it.

It is how we facilitate — or fail to facilitate — those interactions that’s the salient bit.

Visual Bias

Before we get into the technical aspects, we must consider the overwhelming visual bias that permeates software creation.

We live in a highly visual culture, so our outputs unconsciously reflect and reinforce that. What we need to do here is be aware of and think through the non-visual considerations.

While not all disabilities are blindness or low vision conditions, a lot of the core interactions of an LLM chat do need specific accommodations for these two types of disability.

Recall the social model of disability here, and how it applies to the main flow of using an LLM-based chat. You:

  1. Type some words into an input and send it as a message,
  2. Wait for the LLM to parse your message, then
  3. Read the message the LLM generated.

Let’s demonstrate at a high level what someone who can fully see the screen experiences compared to someone who uses a screen reader and cannot see the entire screen, or large portions of it:

(Large preview)

Visually, we can see the LLM reply appear. Someone who uses a screen reader does not know new content has appeared because:

  • Their focus is still on the input that sent the prompt.
  • No non-visual mechanism is present to signal new content showed up.

So, how do we create an experience for someone who uses a screen reader that is equivalent to the experience of someone who can fully see the screen? To start, we use live regions.

Use Live Region Announcements

Live region announcements will enable someone to know what the LLM has responded with without having to move focus from the input used for prompting. This is equivalent to someone who can see the screen, glancing upwards to read the LLM response when it appears.

First, read this excellent article by TetraLogical if this is your first attempt at getting live region announcements working. They’re tricky to get right, and this article sets the record straight.

One common pitfall I want to call specific attention to is applying aria-live on each LLM response message. Don’t do this. Instead, use a dedicated live region area to announce each message’s content.

The live region area should be placed at the end of the DOM and used for all live region messaging. This method is far more stable and reliable, which is important for ensuring everyone understands what the experience is communicating.

With that established: What we want to do is trigger a live region announcement when the LLM’s chat reply shows up in full on the screen.

(Large preview)

This creates an outcome where you can read the message if you can see the screen, and hear or feel the message if you cannot.

Usability Considerations

There are a few things to do when creating a good user experience for the people who rely on these live region announcements. These considerations mean the difference between a compliant-yet-unusable outcome and something that genuinely works for the person using it.

Only Announce The Completed Message

Attempting to announce the message as it streams in may create a juddering and confusing experience that’s difficult for someone to parse.

It is better to wait until the message has completed streaming, and then announce the message’s content in full.

(Large preview)

Let Someone Know Their Request Is Processing

Responses from an LLM often take time to complete. Generally speaking, there are three ways to show a response is still processing:

  1. Using text,
  2. Using a loading animation,
  3. Using text and a loading animation.

These experiences are typically only shown visually. If you cannot see the screen, it may lead to it feeling like the experience has stalled or crashed — especially if the response is for a large or complicated request that takes a while to work through.

To address this, we’ll want to use live region announcements:

If text is visually shown on the screen: Ensure a companion live region announcement is also triggered at the same time the loading message text is displayed. The text string for the live region announcement should be the same as what is shown visually, to be sure to ensure parity of experience:

(Large preview)

If a text-less loading animation is shown on the screen: Ensure the loading state is also communicated to assistive technology.

A live region announcement that mentions the reply is loading after a set amount of time will facilitate an equivalent experience for this interaction. It doesn’t have to be too verbose; the word “Loading” can suffice:

(Large preview)

Triggering the loading message five seconds after prompt submission seems to be the sweet spot, according to the inclusive user research I evaluated. You can then have your logic repeat the loading message every five seconds until the LLM completes its actual reply.

Be sure to also announce the reply immediately as it finishes streaming. You don't need to wait until the next five-second loading message cycle is complete.

Also consider randomly selecting from a set of different phrases that communicate that a response is being formulated after the initial “Loading” message is dispatched. This helps in scenarios where someone may think the loading experience itself has crashed or stalled.

(Large preview)

A Note About ariaNotify()

We’ll eventually be able to replace the technique TetraLogical outlined with ariaNotify(). It is a much-needed and welcome addition to the platform, and will drastically lower the hacks and headaches many developers encounter when creating and managing live region announcements.

Baseline lists the ariaNotify() method as having cross-browser availability at the time of this article’s publication. However, there are two factors to consider here:

  1. The presence of evergreen browsers does not automatically guarantee something is automatically available. There are a whole host of factors that can conspire to keep someone from using the latest and greatest browser version.
  2. Many people who rely on assistive technology have upgrade hesitancy. This is a learned behavior, as you want to be risk-averse and avoid breaking the primary mechanism you use to operate a desktop, laptop, tablet, or smartphone.

Also consider that while a polyfill for ariaNotify() is available, you need to factor in whether it has both:

  1. The full, wide range of support in the actual compared to the dedicated DOM live region technique.
  2. A support policy that is conducive to your organization’s level of accessibility buy-in.

What this realistically means is holding off for a while on ariaNotify() in favor of the tried and true TetraLogical live region technique.

Use Headings

Headings are the most popular way people who use screen readers navigate around an experience. Because of this, I advocate for inserting visually-hidden headings before each chat message.

Here’s a simplified version of that in code:

<article> <hX class="sr-only" aria-label="You said/{agent name} said" tabindex="-1"> You said:/{agent name} said: </hX> <div> {message body} </div> </article>

To break this down:

  • The article element is used to encapsulate each individual message.
  • hx is a placeholder for the heading level. I’m using an h2 in our actual context example, as the h1 will be the chat session’s title.
  • class="sr-only" applies a treatment that hides the text visually, but ensures screen readers can access it.
  • aria-label repeats the heading element’s string value verbatim. This handles a couple of screen reader bugs, ensuring the vital name part of WCAG’s Name, Role, Value success criterion is present if a heading receives focus — more on that in a bit.
  • tabindex="-1" is the mechanism that allows said heading to receive focus.
  • {agent name} is a placeholder for the name of your LLM. Real-life examples are Claude, Copilot, ChatGPT, etc. I‘m going to call my example coding agent “Cody” because I’m clever and funny.

And here’s what the code might look like using the actual content from a human question and an LLM response:

<article> <h2 class="sr-only" aria-label="You said:" tabindex="-1"> You said: </h2> <div> what are the strengths and drawbacks of using utility css? </div> </article> <article> <h2 class="sr-only" aria-label="Cody said:" tabindex="-1"> Cody said: </h2> <div> Utility CSS trades cleaner global styling for faster, more explicit component styling. It works best with component-based frameworks, where repeated class groups can be encapsulated. It is less attractive when semantic HTML must stay visually uncluttered or styling changes independently from templates. </div> </article>

So, why the “You said:”/”Cody said:” prefix? Remember the visual bias. Visually, we can see the left- or right-hand alignment of messages, used to indicate who is saying what.

The prefix serves the same purpose: identifying the message’s owner. This allows people to quickly move through a conversation the same way someone might quickly scroll upwards to re-establish context, checking who said what and when.

Use Landmarks, But Not Too Many

Using landmark regions to wrap the larger overall areas of your LLM chat experience helps people understand its overall layout. It can also enable quick screen reader navigation to each area:

  • The nav element contains a list of sessions.
  • The main element contains the session messages.
  • The form element contains the input you use to write prompts.
  • The aside element contains a sidebar used to display code or other generated content.

Landmarks allow someone who cannot see the screen to learn about its overall makeup. This is the equivalent of how we quickly move our eyes and tilt our heads to different overall areas when scanning the screen to initially learn the overall layout of an experience.