|

Making Immersive LLM Chat Experiences More Accessible — Smashing Magazine

As LLM chat patterns become more widespread, making them accessible is essential to ensuring everyone can access the information, tools, and opportunities they offer. Drawing on his work improving these interfaces, Eric Bailey shares practical ways to make chat-based experiences on the web more accessible and usable for people who rely on assistive technology.

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:

(Large preview)

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.

(Large preview)

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:

  • Type some words into an input and send it as a message,
  • Wait for the LLM to parse your message, then
  • 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.

    (Large preview)

    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:

  • Using text,
  • Using a loading animation,
  • 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:

  • 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.
  • 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:

  • The full, wide range of support in the actual compared to the dedicated DOM live region technique.
  • 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:

    You said:/{agent name} said:

    {message body}

    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:

    You said:

    what are the strengths and drawbacks of using utility css?

    Cody said:

    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.

    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.

    (Large preview)

    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:

    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.

    Annotation courtesy of the GitHub Annotation Toolkit. (Large preview)

    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:

    (Large preview)

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

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

    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.

    (Large preview)

    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:

  • The entire overall content area is one tab stop, and
  • 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 content the LLM generated — a data attribute such as data-last-message can be really helpful for managing this.

    Using F6 and Shift+F6 keypresses allows someone to hop from the chat input to what the LLM outputs, then back again. This enables rapid keyboard-based review and iteration on an LLM’s output.

    (Large preview)

    Double-Check Zooming

    Adjust the text size of your experience and double-check that content is not clipped, or that there’s horizontal overflow.

    People increase the text size of the experiences they use, and more than you’d probably guess. They also resize application windows. Also consider that most LLM-based chat designs tend to use a smaller text size to allow for more information shown on screen.

    Because of these two factors, it makes sense to evaluate how your experience holds up when the browser font size is increased.

    Provide Preferences

    Customization lives at the heart of accessibility. It’s only natural that LLM-based chat experiences have preferences to better accommodate all the different ways people use them.

    In that vein, thoughtful preference offerings can go a long way to helping out people with access needs. Here are some that I’ve found to be particularly useful.

    Set Announcement Granularity

    Some LLMs perform interim actions that are circumstantially less important than the messages they ultimately generate. Here, consider programming-oriented LLMs. They can perform actions like researching and reasoning, code search, and tool calls.

    These secondary actions typically have a smaller, less visually important treatment compared to the messages that contain the main LLM response content. This is because the results are more important than the methodology when using this type of interaction.

    (Large preview)

    Announcing each of these secondary actions could lead to a confusing or overwhelming experience, especially if multiple actions are clustered and fire off live messages in rapid succession.

    To address this, you can allow people to circumstantially switch off the announcements for this type of ancillary information. The content in the conversation is still present; it just isn’t announced via a live region. This is equivalent to how someone who can see the screen can skim over them.

    (Large preview)

    Letting someone re-enable these types of announcements also grants parity in experience, in that it allows them to know the actions are happening if they want a more information-intensive experience. This is equivalent to someone who can see the screen, intentionally paying close attention to the LLM’s actions.

    It might also be worth investigating letting people toggle visually hiding and showing these less important announcements, as well. This can help people with attention or cognitive considerations, who may be less familiar with technical jargon.

    Disabling Animations

    Loading animations and large animated effects, like streaming conversations and side panels expanding and collapsing, can be distracting for some people with attention and cognitive considerations. That can go a long way toward breaking someone’s flow.

    To address this, you can provide a toggle to turn off animations, reducing their transition time to zero.

    You should also strive to be a respectful and courteous steward of the experience. Default your animation toggle to an off state if a request for a reduced motion experience has been in the device operating system. You can use the prefers-reduced-motion media feature to achieve this. Also consider a setting that can re-enable animation, even if a reduced motion request is present.

    Disabling Sound

    Some people can get distracted by, or have their focus disrupted by, sound effects. This is similar to animation considerations.

    Letting people turn off app-specific sound effects means they don’t have to mute their entire device. Here, consider things like other apps playing flow state-inducing music or white noise that they might be using to help them focus.

    Alternatives for sound-based notifications include, but are not limited to:

    • Native notifications. This places alerts in a known, expected, and centralized area. Many people already have existing preferences set up for, as well as habits and muscle memory for checking.
    • Badging. Similar to native notifications, many people are familiar with a small dot placed over the application icon being used to show an update.
    • Email. This places updates in another place people also have existing habits for. It is less immediate than a notification, and also threaded into a larger asynchronous communication stream.
    • In-app banners. This is also an asynchronous update technique, one that is intentionally less prioritized than email. Here, someone checks in on progress on their own time, but has a highly visible and consistent way to see if a task has completed or not.

    Make Keyboard Shortcuts Re-Bindable

    I am of the school of thought that any experience that uses keyboard shortcuts also needs to allow both editing and being switched off, both in bulk and on a shortcut-by-shortcut basis.

    This allows people to:

    • Undo conflicts that may arise from other applications they may have installed, and also
    • Set keyboard shortcuts to something that better matches their expectations.

    You may be quick to think of conflicts with assistive technology, but I’d also like to take this space to remind you of different language keyboard layouts.

    Consider How Special Content Is Announced

    Live region messages are plain text strings. These strings are announced by screen readers in full in one go from start to end.

    Semantic information that comes for free by using the appropriate HTML element isn’t present in live region announcements—it’s stripped out. Because of that, we need to think about how we could make our live region content both clear and useful.

    Note: Parsing and processing strings — including strings used for live regions — is both a brittle and finicky experience. It is also invisible and unglamorous infrastructure.

    The act of creating and maintaining this specialized logic often requires far more time and investment than most organizations are willing to expend. Consider keeping up with refactors, localization, and the maintenance budget this will all require.

    Only take on special content announcements if you’re absolutely sure you can stay on top of it.

    Each type of special content has its own considerations. For my work, I created special cases for tables and blocks of code:

    Tables

    A table’s cell content will sound odd when announced without the companion semantic structure conferred by elements such as table, th, tr, and td. Relationships between columns and rows aren’t present in a live region announcement, either.

    Because of this, we can convert the live region announcement of tables to use a concise description of the number of rows and columns, plus a general description and overall trends it might highlight.

    Example output:

    Table containing a performance audit, with 4 columns and 36 rows. Audit is broken down by severity.

    Code Blocks

    A block of code sounds like gibberish when the line breaks are stripped away, and the content is flattened into a single string.

    And if it’s a large block of code, it can be a tedious amount of gibberish to parse — nobody wants to sit through an announcement of 2,376 lines of code, painstakingly announced character-by-character.

    To accommodate this, we can convert the live region announcement of code blocks to use the filename or file type, plus how many lines of code the block contains.

    Example output:

    settings.json, 738 lines.

    Also note here that people can revisit the actual message content itself. The actual message content will ideally have the appropriate semantics applied to it. This means things like tables being announced as, and navigated through as, table content.

    In this way, the live region message is a context-setting preview of what to expect. This is the equivalent of quickly visually skimming a message as it is streaming in, then scrolling back to review its content with more attention afterward.

    Ask People What They Think

    All of the previous is built atop feedback from testing different approaches and noting what works, and what could be improved. You want to facilitate disabled people being the experts in their own lived experience, and not treat others’ advice as the more credible source.

    Inclusive user research is a vital way of determining if an experience is actually usable. Centering disabled people’s observations and perspectives is a way to take an experience beyond baseline compliance and create something usable and equitable.

    Wrapping Up

    It is vital to ensure that everyone can use chat-based LLM interactions as the pattern continues to proliferate. Although this is not an exhaustive list of everything you’ll need to do, the content above covers a wide swath of the main places accessibility barriers and friction may show up.

    It is my sincerest hope that the improvements I’ve learned about and implemented can help you with your efforts to make chat-based experiences on the web more accessible and usable for people who use assistive technology to get what they want or need online.

    Thank you to Sarah Higley, whose research and efforts in this area informed key parts of this work.

    (gg, yk)

    Source link

    Similar Posts