Conversations

A list of chats for a sidebar, with a new chat button, per-chat menus and groups.

Accessibility

What Conversations does for you, and the keyboard and screen reader support you add to it.

What Conversations renders

The root is a <ul> and each conversation is an <li> with a click handler. A screen reader announces a list and its item count, and reads each label as text.
  • The items are not buttons or links, and they have no tabindex. A keyboard user cannot reach or select a conversation, and a screen reader does not say an item can be activated.
  • The selected item is marked only by its background. Nothing like aria-current tells a screen reader which chat is open.
  • A group heading is plain text inside the list, not a heading element, and a collapsible heading is a clickable <div> with no expanded state.
  • The “New chat” button is a real <button type="button">, focusable and named by its label.

Making items reachable by keyboard

Each item passes tabIndex, role, aria-* and onKeyDown from its entry in items to its <li>. Keep the selection in your state and add them yourself.
1
const items = chats.map((chat) => ({
2
key: chat.id,
3
label: chat.title,
4
tabIndex: 0,
5
"aria-current": chat.id === activeKey ? "page" : undefined,
6
onKeyDown: (event) => {
7
if (event.key === "Enter" || event.key === " ") {
8
event.preventDefault();
9
setActiveKey(chat.id);
10
}
11
},
12
}));
13
14
<nav aria-label="Chats">
15
<Conversations items={items} activeKey={activeKey} onActiveChange={setActiveKey} />
16
</nav>
  • Wrap the list in a <nav> with a name, so a screen reader user can jump to the chat history from the landmarks list.
  • Give items a visible focus style through the item key of classNames, like a focus-visible: outline. The item has no focus style of its own.
  • shortcutKeys helps keyboard users who know the keys, but it is not a replacement for focusable items: nobody can discover a shortcut by tabbing.
The default “more” trigger is an icon with a click handler, not a button. It cannot be focused, it has no name, and it is hidden until the pointer is over the item.
  • Pass a trigger that is a real button with an aria-label that names the chat, like “Actions for Oblomov, chapter one”. Use the function form, which gets the conversation.
  • Make that button visible on focus as well as on hover, so a keyboard user can see what they tabbed to.
  • Once open, the menu is a Menu: arrow keys move between actions, Enter picks one and Escape closes it.
  • Every action the menu offers should also be reachable another way if the trigger stays hidden, for example from the chat's own header.

Labels and icons

  • A string label is also the item's title, so a truncated chat name can be read in full on hover. A node label gets no title. Pass a string, or set title on the item yourself.
  • An item icon is read as part of the item. If it only decorates, give it aria-hidden. If it carries meaning, like a pinned or shared chat, give it a label or put the word in the label text.
  • A disabled item looks dimmed and ignores clicks, but nothing tells a screen reader it is disabled. Add aria-disabled to the item.
  • A disabled “New chat” button ignores clicks but stays focusable, and is not announced as disabled. Explain why in nearby text, or leave creation out while it cannot be used.

Contrast

The selected item and the hover state share one light fill, and the “New chat” button uses the primary color at low opacity.
  • Check that the selected item is distinct from the rest without relying on color alone. A bold label or a leading marker survives a monochrome display.
  • The “New chat” label is drawn in the primary color on a tint of itself. With a light custom primary, check that pairing against 4.5:1.