Skip Navigation

Skip links let keyboard and screen-reader users jump straight to the main regions of the app, past repeated navigation. SkipNavigation is an opt-in helper for this. It is not rendered by Chat, so you add it yourself as the first focusable element in your layout.

For the prop reference see the Accessibility guide.

Basic setup

Give your key regions stable DOM ids, then render SkipNavigation first with those ids ordered by keyboard priority (the composer usually comes first).

import {
  Channel,
  Chat,
  MessageInput,
  MessageList,
  SkipNavigation,
  Window,
} from "stream-chat-react";

export const App = ({ client }) => (
  <Chat client={client}>
    <SkipNavigation
      targetIds={["message-composer", "message-list"]}
      getLinkLabel={(targetId) =>
        targetId === "message-composer"
          ? "Skip to message composer"
          : "Skip to messages"
      }
    />
    <Channel>
      <Window>
        <div id="message-list">
          <MessageList />
        </div>
        <div id="message-composer">
          <MessageInput />
        </div>
      </Window>
    </Channel>
  </Chat>
);

The links keep native hash navigation (href="#message-composer") and also move focus programmatically. If a target is not naturally focusable, SkipNavigation applies tabindex="-1" temporarily and removes it on blur, so focus lands reliably without leaving a stray tab stop behind.

Skip links are conventionally hidden until focused. Each link carries the str-chat__skip-navigation-link class; a className you pass is merged in. A common pattern is to position them off-screen and reveal them on :focus:

.str-chat__skip-navigation-link {
  position: absolute;
  left: -9999px;
  z-index: 1000;
}

.str-chat__skip-navigation-link:focus {
  left: 1rem;
  top: 1rem;
}

When you use ChatView to switch between the channel list and the thread list, the relevant targets differ per view. Read the active view from useChatViewContext and compute targetIds accordingly.

import { SkipNavigation, useChatViewContext } from "stream-chat-react";

const ViewAwareSkipNavigation = () => {
  const { activeChatView } = useChatViewContext();

  const targetIds =
    activeChatView === "threads"
      ? ["thread-composer", "thread-list"]
      : ["message-composer", "channel-list"];

  const getLinkLabel = (targetId: string) => {
    switch (targetId) {
      case "message-composer":
      case "thread-composer":
        return "Skip to message composer";
      case "channel-list":
        return "Skip to channel list";
      case "thread-list":
        return "Skip to thread list";
      default:
        return `Skip to ${targetId}`;
    }
  };

  return <SkipNavigation targetIds={targetIds} getLinkLabel={getLinkLabel} />;
};

Render ViewAwareSkipNavigation as the first child inside ChatView and give the corresponding regions matching ids. Because SkipNavigation renders null when targetIds is empty, a view with no available targets simply shows no links.

Pass DOM ids without the leading #. You can intercept activation by passing an onClick handler and calling event.preventDefault(), for example to run analytics or a custom focus routine.