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.

I am hesitant to wrap chat messages in section or other landmark elements. The reason for this is that they “pollute” the list of landmarks a screen reader can generate, making navigation more laborious. This is especially relevant for long-running sessions where many messages are present.

Compare the following and ask yourself which one would be easier to move through with your keyboard if trying to jump from the prompt content area back to the sessions area:

Here, our visually hidden heading elements serve as a dedicated navigation mechanism for moving through the LLM conversation.

Use Skip Links And Composite Widgets

In addition to headings and landmarks, there are two other ways to let people get around quickly:

1. Skip Links

Skip links — sometimes referred to as skip navigation — allow you to quickly jump to another area on the same page or view.

Skip to main content links are commonplace on many mature websites and web apps. Immersive LLM chat experiences should be no different.

Place a skip link in the DOM that allows someone to jump to the main conversation area before any other DOM content that is shown visually on the screen.

Also consider using additional skip links that enable:

  • Quickly jumping to the prompt input, as well as
  • Bypassing the side panel list of conversations to quickly access the area where profile information and settings are located.

2. Composite Widgets

Composite widgets are used when a discrete section of content contains multiple interactive elements. Focus is then managed within this section by the developer, so that:

  1. The entire overall content area is one tab stop, and
  2. Its child interactive elements are navigated via keyboard.

For the purposes of an immersive LLM chat experience, we want to make conversation lists composite widgets. This enables someone to quickly navigate both within and past the entire list.

Here, consider the use case of someone who exclusively relies on a keyboard to navigate. Compare pressing Tab 50 times to get through each of the 50 conversation sessions you’ve created versus pressing Tab once. Now consider motor control conditions where those keypresses can be fatiguing, or even painful to perform.

Once someone places focus on a conversation list composite widget, they can use the Up and Down arrow keys to navigate through each conversation session list item.

(Large preview)

As a quality-of-life improvement, I also like to support the following keyboard interactions:

  • Home and End to jump to the topmost and bottommost conversation session list items.
  • Typeahead, to match the characters typed with the starting characters of the conversation list item’s title. This is equivalent to what you can do with items within a select menu.
Don’t Use A Feed Pattern

To save you a lot of headaches and prototyping: Corner cases around focus management and usability expectations for people who rely exclusively on keyboards work against you if you try to go with a feed pattern approach.

There’s no need to unnecessarily overcomplicate things. The article elements’ wrapping messages with visually-hidden headings approach that I’ve described earlier is both simple and effective.

People who use assistive technology understand the simpler approach, and it involves far less effort to maintain. It’s a win-win!

Consider Supporting Control + F6 Navigation

Pressing F6/Shift+F6 to hop around different areas of the app is an older navigation technique some people who use Windows screen readers employ when navigating through installable desktop apps. Think of it a lot like navigating by landmark.

(Large preview)

LLM chat experiences are effectively apps in their own right. Because of this, we can recreate the F6 navigation experience and accommodate those who might attempt it. That means:

  • Using Control+F6/Control+Shift+F6 for web experiences. Here, Control serves as a focused modifier key, as the browser itself already has F6 navigation support built into it.
  • Using F6/Shift+F6 for native app experiences built using web technologies. This is because a dedicated, installable app has the expectation that it will operate like one, regardless of how it was built.

How To Build F6 Navigation

There’s no secret sauce here. For starters, place an id on each landmark, as well as tabindex="-1". Also ensure each landmark other than main has an accessible name, granted via either aria-label or aria-labelledby.

After that, have an event listener set up to recognize Control+F6 and Control+Shift+F6 key presses. Set focus to:

  • The next landmark in sequence when Control+F6 is pressed.
  • The previous landmark in sequence when Control+Shift+F6 is pressed.

And voilà! We now have web-based F6 navigation.

How To Socialize F6 Navigation

As it is an older technique, people who are younger or less experienced with screen readers may not know F6 navigation exists.

Consider socializing the capability in the accessibility portion of your application’s settings menu to address this lack of awareness. Also consider exposing and making F6 navigation re-bindable as another discovery technique, provided your application supports keyboard shortcut rebinding.

Special Considerations For Where To Set Focus Using F6 Navigation

Again, this advice only applies to an immersive LLM chat experience. Help agent popups and other less-sophisticated experiences have different expectations.

There are two areas that deserve more finesse when considering where to set focus when F6 navigation is used: the prompting input and the conversation.

Setting focus directly on the input used to type prompts — and not its parent form landmark element — allows someone to quickly get into the action.

Similarly, setting focus on the visually hidden heading of the last message displayed allows them to quickly jump to the