How do I move around this chat with the keyboard?
Chat - Accessibility
Learn how MUI X Chat implements keyboard navigation, landmarks, and screen reader announcements.
Keyboard navigation
The message list is a single Tab stop: a roving tabindex over the role="article" messages keeps only one message in the tab order at a time, so tabbing from the composer to the rest of the application never walks through every message.
Tab into the list (a single stop), Arrow Up/Down between messages, Enter to drill into the focused message's links and buttons, Escape to come back, Tab onward to the composer.
| Key | Action |
|---|---|
| Tab / Shift+Tab | Enter or leave the message list (a single stop) |
| Arrow Up / Arrow Down | Move focus to the previous / next message |
| Home / End | Move focus to the first / latest message |
| Page Up / Page Down | Native scrolling (kept unbound so a message taller than the viewport stays readable by keyboard) |
| Enter | Drill into the focused message's controls (links, copy buttons, tool output, actions) |
| Escape | Return from a message's controls to the message |
Before the user interacts, the tab stop tracks the newest message. The tab stop is remembered per list, so leaving and re-entering the message list returns focus to the same message.
Drill-in lifecycle
Interactive content inside messages—links in Markdown, code-block copy buttons, tool and reasoning disclosures, source and file links—stays out of the tab order until the user drills into the focused message with Enter, and leaves it again on Escape.
All controls remain mouse-clickable throughout.
Message actions are additionally hidden (visibility: hidden) until the message is hovered or drilled into.
When the user drills back out with Escape, focus returns to the message that owned the controls. Because the tab stop is remembered per list, re-entering the list later restores focus to the same message.
Landmarks
The chat surface exposes labeled landmarks so assistive technology can jump between its regions:
- The thread is a
role="region"labeled by thethreadLandmarkLabellocale key. - The composer is a labeled
form(thecomposerLandmarkLabellocale key). - The conversation list is a
role="navigation"region labeled byconversationListLandmarkLabel. - On small screens, the conversation list opens in a
role="dialog"witharia-modal="true".
Screen reader announcements
- The scroller element has
role="log"andaria-live="polite", so newly arriving complete messages are announced. - A streaming message carries
aria-busy="true"while it streams, hinting assistive technology to defer reading it until it completes. - A visually hidden
role="status"region announces streaming transitions—"Assistant is responding" and "Response complete"—exactly once each, never per streamed token. - Each message is a
role="article"labeled "Message from {author}". - Date dividers use
role="separator".
The announcement strings and labels come from the locale text system, so they localize with the rest of the UI: messageListLabel for the list, responseStreamingStartedAnnouncement and responseStreamingCompletedAnnouncement for the streaming announcer, and threadLandmarkLabel, composerLandmarkLabel, and conversationListLandmarkLabel for the landmarks.
Reduced motion
The loading skeleton's shimmer animation pauses automatically when the user requests reduced motion (prefers-reduced-motion: reduce).
The skeleton itself is decorative and sets no ARIA, so you wire the loading state up explicitly—see Loading and empty states for the aria-busy and role="status" pattern.
Opting out and custom controls
Set enableRovingFocus={false} on the message list to opt out entirely, for example when rendering fully custom rows that manage focus themselves.
Custom interactive content rendered inside a message can participate in the drill-in model with the useMessageContentTabIndex() hook (or useMessageActionable() for full control), both available from @mui/x-chat/headless:
function CustomControl() {
const tabIndex = useMessageContentTabIndex();
return (
<button type="button" tabIndex={tabIndex}>
…
</button>
);
}
Outside a roving message list both hooks leave the natural tab order untouched, so the same component works in standalone message compositions.
See also
- Message list—Accessibility for the message-list keyboard table and drill-in specifics.
- Messages for the summary alongside the message API.
- Conversation list—Accessibility notes for the sidebar's roving listbox.