Skip to content

Chat

A conversation, read in the order it happened.

Preview

Code

The three render the same class names, the same ARIA and the same data attributes. That is checked when this site builds: a class in any of these samples that neither the markup contract nor the shipped stylesheet defines fails the build.

<ol class="wsu-chat" aria-live="polite" aria-label="Conversation with your advisor">
  <li class="wsu-chat__message">
    <span class="wsu-chat__avatar">
      <span class="wsu-avatar wsu-avatar--sm" role="img" aria-label="Pat Nguyen">PN</span>
    </span>
    <div class="wsu-chat__content">
      <div class="wsu-chat__header">
        <span class="wsu-chat__name">Pat Nguyen</span>
        <time class="wsu-chat__time" datetime="2026-11-02T09:14">9:14 AM</time>
      </div>
      <p class="wsu-chat__bubble">Your advising hold clears once you're under 12 credits or I approve the overload.</p>
    </div>
  </li>

  <li class="wsu-chat__message wsu-chat__message--sent">
    <span class="wsu-chat__avatar">
      <span class="wsu-avatar wsu-avatar--sm" role="img" aria-label="You">EE</span>
    </span>
    <div class="wsu-chat__content">
      <div class="wsu-chat__header">
        <span class="wsu-chat__name">You</span>
        <time class="wsu-chat__time" datetime="2026-11-02T09:20">9:20 AM</time>
      </div>
      <p class="wsu-chat__bubble">Please approve the overload, I need CSC 2110 this term.</p>
      <span class="wsu-chat__status">Delivered</span>
    </div>
  </li>
</ol>

Why it works this way

  • An <ol> rather than a stack of divs, for the same reason Timeline is one: the sequence is the information (1.3.2). aria-live="polite" marks it as a region that grows, so an application that pushes a new message onto items gets it read out without moving focus, while a transcript that is only ever rendered once announces nothing extra, because a live region only speaks to changes made after it exists.
  • The live region sits on the list itself rather than on a wrapping role="log", because a non-list role on the ol would leave its own li children with no list to belong to.
  • Which side a bubble sits on is a visual convenience, not the only way to tell who is speaking. The name is always real text in the header, sent or received alike (1.4.1).
  • This is on the internal template only. The example is a student asking their advisor about a hold, and an advisor relationship is exactly the thing the anonymous public catalog page does not have: nobody has signed in on it, and there is no one for a message to be from.

Accessibility

  • aria-live="polite" on the list, so new messages are announced without moving focus (4.1.3).
  • The name is text in the header regardless of which side the bubble is on (1.4.1).
  • Each avatar is a real Avatar, named the same way it is everywhere else (1.1.1).

Success criteria in brackets refer to WCAG 2.2. Every one of them is checked by axe-core in both themes at three viewport widths before this page can be published. See Accessibility for what the gates are.

Props

Blade

AttributeDefaultWhat it does
items[][{ from, name, avatar, time, datetime, message, status }]. from is sent or received.
label'Conversation'Names the log for screen reader users.

Vue

PropType and defaultWhat it does
itemsArray, [][{ from, name, avatar, time, datetime, message, status }]. from is sent or received.
labelString, 'Conversation'Names the log for screen reader users.

In the markup contract

contract/markup-contract.json is extracted from the two static templates and every package tests against it. This is what it records for .wsu-chat.

TemplateElementsAccessibility attributesUses
internal apps/examples/static-html/index.htmlolaria-labelaria-live1