Accessibility pattern · Communication

Chat

The conversation is a role=log region: a screen reader reads each new message once, politely, while focus stays in the message box. Every message carries its sender and time as text, so who said what never depends on which side of the screen a bubble sits.

WCAG criteria
7
Keyboard rules
5
Checked with
axe, keyboard and the inspector

Live demo

Try it

Use it with a mouse, a keyboard or a screen reader. The inspector beside it shows what the browser tells assistive technology as you go: focus, state changes and announcements.

Kesar Tea support

Online. Replies in about a minute.

Today

  1. Priya, Kesar Tea

    Hello Asha, I'm Priya from Kesar Tea. How can I help today?

  2. You

    My order KT-4821 shows as delivered, but nothing has arrived.

  3. Priya, Kesar Tea

    Sorry about that. Let me check with the courier.

  4. Priya, Kesar Tea

    The courier left it at your building's security desk at 9:40 this morning.

  5. You

    Found it, thank you! Can I change the address for my next order?

  6. Priya, Kesar Tea

    Yes. Open the order in your account and choose Change address before it ships.

Enter sends. Shift + Enter starts a new line.

The demo works without JavaScript only as far as its HTML does; the inspector needs JavaScript.

Keyboard

Keys it answers to

Every action works without a pointer.

KeyWhat it does
TabMoves to the conversation, then to Jump to latest when it shows, then to the message box and Send.
Arrow Up or Arrow DownWith the conversation focused, scrolls the history; Page Up, Page Down, Home and End scroll further.
EnterIn the message box, sends the message. Focus stays in the box, ready for the next one.
ShiftEnterIn the message box, starts a new line instead of sending.
Enter or SpaceOn Jump to latest, scrolls to the newest message and puts focus on the conversation.

Screen readers

What it announces

Written from the roles, names and states in the markup.

WhenExpected announcement
Tab reaches the conversationConversation with Kesar Tea support, log
Tab reaches the message boxMessage, edit text, multi line. Enter sends. Shift + Enter starts a new line.
You send a messageYou 10:07 Can I pay by UPI?
Priya starts typingNothing: the typing indicator is shown, not announced
Her reply arrivesPriya, Kesar Tea 10:07 Thanks for waiting. I have added a note to your order.
Enter is pressed with nothing typedWrite a message first.

These are expected announcements, not recordings. Wording and order differ between screen readers and browsers.

Code

Copy the code

The exact files this demo runs on. The styles are served with a prefix that keeps this site's own styles out of the demo; what you copy is the original.

The markup

<section class="ap-chat" data-ap-chat aria-labelledby="chat-name">
  <div class="ap-chat__top">
    <span class="ap-chat__logo" aria-hidden="true">
      <svg viewBox="0 0 24 24" focusable="false"><path d="M5 19c0-8 5-13 14-14 0 9-5 14-13 14Z"/><path d="M5 19 13 11"/></svg>
    </span>
    <div class="ap-chat__who">
      <h3 class="ap-chat__name" id="chat-name">Kesar Tea support</h3>
      <p class="ap-chat__presence"><span class="ap-chat__dot" aria-hidden="true"></span>Online. Replies in about a minute.</p>
    </div>
  </div>

  <div class="ap-chat__body">
    <div class="ap-chat__log" role="log" aria-live="polite" aria-label="Conversation with Kesar Tea support" tabindex="0">
      <p class="ap-chat__day">Today</p>
      <ol class="ap-chat__list">
      <li class="ap-chat__msg ap-chat__msg--them">
        <p class="ap-chat__meta"><span class="ap-chat__from">Priya, Kesar Tea</span> <time datetime="2026-10-05T10:02+05:30">10:02</time></p>
        <p class="ap-chat__text">Hello Asha, I'm Priya from Kesar Tea. How can I help today?</p>
      </li>
      <li class="ap-chat__msg ap-chat__msg--me">
        <p class="ap-chat__meta"><span class="ap-chat__from">You</span> <time datetime="2026-10-05T10:03+05:30">10:03</time></p>
        <p class="ap-chat__text">My order KT-4821 shows as delivered, but nothing has arrived.</p>
      </li>
      <li class="ap-chat__msg ap-chat__msg--them">
        <p class="ap-chat__meta"><span class="ap-chat__from">Priya, Kesar Tea</span> <time datetime="2026-10-05T10:04+05:30">10:04</time></p>
        <p class="ap-chat__text">Sorry about that. Let me check with the courier.</p>
      </li>
      <li class="ap-chat__msg ap-chat__msg--them">
        <p class="ap-chat__meta"><span class="ap-chat__from">Priya, Kesar Tea</span> <time datetime="2026-10-05T10:05+05:30">10:05</time></p>
        <p class="ap-chat__text">The courier left it at your building's security desk at 9:40 this morning.</p>
      </li>
      <li class="ap-chat__msg ap-chat__msg--me">
        <p class="ap-chat__meta"><span class="ap-chat__from">You</span> <time datetime="2026-10-05T10:06+05:30">10:06</time></p>
        <p class="ap-chat__text">Found it, thank you! Can I change the address for my next order?</p>
      </li>
      <li class="ap-chat__msg ap-chat__msg--them">
        <p class="ap-chat__meta"><span class="ap-chat__from">Priya, Kesar Tea</span> <time datetime="2026-10-05T10:06+05:30">10:06</time></p>
        <p class="ap-chat__text">Yes. Open the order in your account and choose Change address before it ships.</p>
      </li>
      </ol>
    </div>
    <button type="button" class="ap-chat__jump" data-ap-chat-jump hidden>
      <svg viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="M12 5v14"/><path d="m6 13 6 6 6-6"/></svg>
      Jump to latest
    </button>
  </div>

  <p class="ap-chat__typing" data-ap-chat-typing><span class="ap-chat__typing-in" hidden><span class="ap-chat__dots" aria-hidden="true"><span></span><span></span><span></span></span> Priya is typing…</span></p>

  <form class="ap-chat__composer" novalidate>
    <label class="ap-chat__label" for="chat-input">Message</label>
    <div class="ap-chat__row">
      <textarea class="ap-input ap-chat__input" id="chat-input" name="message" rows="1" aria-describedby="chat-hint" autocomplete="off"></textarea>
      <button type="submit" class="ap-btn ap-btn--primary ap-chat__send">
        <svg class="ap-btn__icon" viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="M4 12 20 4l-4 16-4-7-8-1Z"/><path d="m12 13 8-9"/></svg>
        Send
      </button>
    </div>
    <p class="ap-error ap-chat__error" id="chat-error" hidden>
      <svg class="ap-chat__icon" viewBox="0 0 24 24" aria-hidden="true" focusable="false"><circle cx="12" cy="12" r="9"/><path d="M12 7.5v5"/><path d="M12 16.5h.01"/></svg>
      <span>Write a message first.</span>
    </p>
    <p class="ap-hint ap-chat__hint" id="chat-hint"><kbd>Enter</kbd> sends. <kbd>Shift</kbd> + <kbd>Enter</kbd> starts a new line.</p>
    <p class="ap-chat__vh" aria-live="polite" data-ap-chat-say></p>
  </form>
</section>

WCAG 2.2

What it meets

The success criteria this pattern takes care of, and how.

  • 1.3.1 Info and Relationships Level A

    Messages are list items whose sender and time are text and a time element, so the structure is in the markup, not only in bubble positions.

  • 1.4.1 Use of Color Level A

    Your messages and Priya's differ in side and color, and each also says You or Priya, Kesar Tea.

  • 2.1.1 Keyboard Level A

    The history is a focusable region, so it scrolls with the keyboard; sending, new lines and jumping to the latest all have keys.

  • 2.4.3 Focus Order Level A

    Sending keeps focus in the message box, and Jump to latest puts focus on the conversation it scrolled, never on the page top.

  • 3.3.1 Error Identification Level A

    An empty message is not sent silently: an error under the box says what to do, and it is announced.

  • 4.1.2 Name, Role, Value Level A

    The history is a named log, the message box has a label and a hint, and Send is a real button.

  • 4.1.3 Status Messages Level AA

    New messages reach screen readers through the polite log without moving focus; the typing indicator is left out on purpose.

Usage

When to use it

Use it

  • Live conversations with a person or a bot, where new messages arrive while people are typing.
  • Comment-style streams that grow at the bottom and are read in order.

Use something else

  • A one-off contact form with a reply by email: a plain form and a status message are simpler.
  • A feed people scroll through to browse, with no typing: use the feed pattern.
  • Very busy rooms with many speakers: announcing every message is overwhelming there; offer a setting to mute announcements.

Common failures

How it usually goes wrong

  • Announcing the typing indicator

    "Priya is typing" spoken every few seconds talks over the person reading. Here it is shown on screen only; the reply itself is what gets announced.

  • Sender shown only by position

    Left and right bubbles mean nothing to a screen reader, and little to someone zoomed in. Each message here starts with who sent it and when.

  • A whole-log live region set to atomic

    aria-atomic="true" on the log rereads the entire conversation for every new message. The log here reads only what was added.

  • Moving focus to new messages

    Jumping focus to each reply throws people out of the message they are writing. Focus stays put; the log does the telling.

  • A scroll area the keyboard cannot reach

    A scrolling div with no tabindex can only be scrolled with a mouse or touch. The log here is a focusable region with a name.

  • Forced scrolling while reading back

    Snapping to the bottom on every message loses the place of someone reading older ones. New messages scroll the view only when it is already at the bottom; otherwise Jump to latest appears.

Notes

Building it

  • role=log is polite and non-atomic by default; aria-live="polite" is repeated on it for older assistive technology.
  • Put only messages inside the log. Anything else added there, a typing indicator or a date divider, is announced as if it were a message.
  • Enter sends only when no input method is composing text (event.isComposing), so people typing Hindi or Japanese through an IME can confirm characters without sending.
  • Times use the time element with a full datetime attribute; show a date divider when the day changes.
  • In a busy chat, give people a setting to turn announcements down or off; aria-live="off" on the log keeps it readable without speaking.

Sources: WAI-ARIA 1.2: the log role · MDN: ARIA live regions

Checked with axe in light and dark themes, at desktop and phone widths, and by keyboard. Not yet tested with every screen reader and browser pair. Report a correction