# Typing indicators

> **Info:** This is release candidate documentation for **Stream Chat React SDK v15**. For the latest stable version, see the [latest version (v14)](https://getstream.io/chat/docs/sdk/react/).

Typing indicators show who is writing a message right now. This page covers the typing events behind them, when they are sent, and how to change the indicator.

## What users get

Bob starts typing in a channel. On the user's screen, his avatar and three animated dots appear at the bottom of the message list, and the subtitle in the channel header changes to "Bob is typing". With two people typing it reads "Bob and Cleo are typing", and with more, "3 people are typing". The indicator in the list stays for two seconds after the last person stops, so it doesn't flicker between words. Screen readers hear the same text.

Nobody sees their own typing.

## How it works across the layers

- **State.** `Channel.keystroke()` and `Channel.stopTyping()` send `typing.start` and `typing.stop` events. When another member's event arrives, the channel stores it in `channel.state.typing`, keyed by user id. The channel's composer keeps the same map in `channel.messageComposer.textComposer.state`.
- **UI.** The text composer calls `Channel.keystroke()` as the user types. `MessageList` renders `TypingIndicator` at the bottom of the list. `ChannelHeader` and `ThreadHeader` render `TypingIndicatorHeader` in their subtitle. `VirtualizedMessageList` renders a typing indicator only when you register one, see [Customizing](#customizing).
- **Layout.** Not involved.

[What channels and threads hold](https://getstream.io/chat/docs/sdk/react/v15/state/channels-and-threads/what-channels-and-threads-hold/) explains the rest of `channel.state`. [Channel UI](https://getstream.io/chat/docs/sdk/react/v15/ui/components/channel-ui/) shows where the indicators sit.

### Typing events

A typing event belongs to a channel. One sent from a thread carries the thread's id as `parent_id`, so the same event can show in the channel or in one of its threads.

#### Sending typing events

>
> **Info:** **Example**
>
> The user types "Hello" in the composer:
>
> 1. Their first keystroke calls `Channel.keystroke()`, which sends `typing.start`.
> 2. Their next keystrokes call it again, but it sends nothing until two seconds have passed since the last `typing.start`.
> 3. They stop typing. A second later, the client sends `typing.stop` by itself.
> 4. If they press Enter instead, the composer sends `typing.stop` before the message.
>

In a thread, the composer calls `Channel.keystroke(thread.id)` and the events carry `parent_id`.

A custom input that writes through `messageComposer.textComposer` sends typing events the same way. An input that doesn't use the composer calls the channel directly:

```ts
// On every change of the input: sends typing.start at most every two seconds
await channel.keystroke();

// When the user sends or abandons the message
await channel.stopTyping();

// The same for a thread
await channel.keystroke(thread.id);
await channel.stopTyping(thread.id);
```

#### Timing

- `typing.start` goes out at most once every two seconds while the user keeps typing.
- `typing.stop` goes out when the user hasn't typed for more than a second, and before a message is sent. The client checks every half second.
- Another member's entry is dropped from `channel.state.typing` seven seconds after their last `typing.start` arrived, in case their `typing.stop` never comes. The client then raises a local `typing.stop` event.
- The message list's indicator waits two seconds after the last person stops before it hides. The header subtitles switch back at once.

#### When typing events are suppressed

`Channel.keystroke()` and `Channel.stopTyping()` send nothing when:

- typing events are off for the channel, by the client configuration or by the channel type, see [Turning it on or off](#turning-it-on-or-off);
- the user's `privacy_settings.typing_indicators.enabled` is `false`;
- the WebSocket connection isn't healthy;
- the channel has no id from the server yet.

The composer also skips `Channel.keystroke()` when the text is empty, and when its `text.publishTypingEvents` setting is `false`.

#### Typing in threads

Sending works in threads: a thread composer sends events with `parent_id`, and every member's `channel.state.typing` receives them. The one exception is the `typing.stop` the client sends by itself after a pause: it carries no `parent_id`. The channel removes the user's entry either way.

Showing them doesn't, in the default components. `TypingIndicator` and `TypingIndicatorHeader` read the typing map of the composer they find in context. Inside `Thread` that is the thread's composer, and only the channel's composer receives typing events. So the indicator in a thread's message list, and the subtitle of `ThreadHeader`, never show anyone typing.

To show typing in threads, register an indicator that reads `channel.state.typing` and filters by `parent_id`. This one works in channels and threads alike:

```tsx
import { useEffect } from "react";
import type {
  ChannelConfig,
  ChannelStateData,
  EventPayload,
} from "stream-chat";
import {
  useChannel,
  useChatContext,
  useStateStore,
  useThreadContext,
  type TypingIndicatorProps,
} from "stream-chat-react";

const typingSelector = ({ typing }: ChannelStateData) => ({ typing });
const typingEventsSelector = ({ typingEvents }: ChannelConfig) => ({
  typingEventsEnabled: typingEvents.enabled,
});

export const ChannelAndThreadTypingIndicator = ({
  isMessageListScrolledToBottom = true,
  scrollToBottom,
}: TypingIndicatorProps) => {
  const { client } = useChatContext();
  const channel = useChannel();
  const thread = useThreadContext();
  const { typing } = useStateStore(channel.state, typingSelector);
  const { typingEventsEnabled } = useStateStore(
    channel.configState,
    typingEventsSelector,
  );

  const names = (Object.values(typing) as EventPayload<"typing.start">[])
    .filter(({ user }) => user?.id !== client.user?.id)
    // In a thread, its own replies; in the channel, events without a thread
    .filter(({ parent_id }) => (thread ? parent_id === thread.id : !parent_id))
    .map(({ user }) => user?.name || user?.id);

  const visible =
    typingEventsEnabled && isMessageListScrolledToBottom && names.length > 0;

  useEffect(() => {
    if (visible) scrollToBottom?.();
  }, [scrollToBottom, visible]);

  if (!visible) return null;

  return (
    <div className="custom-typing-indicator" role="status">
      {names.join(", ")} {names.length === 1 ? "is" : "are"} typing…
    </div>
  );
};
```

Register it with `<WithComponents overrides={{ TypingIndicator: ChannelAndThreadTypingIndicator }}>` around `Channel` and `Thread`. A custom thread header can use the same filter for its subtitle, see the [Thread header](https://getstream.io/chat/docs/sdk/react/v15/recipes/headers/thread-header/) recipe.

## Turning it on or off

Typing events are on when both of these allow them:

- the channel type's `typing_events` setting on the server, see [Typing indicators](https://getstream.io/chat/docs/javascript/typing-indicators/) in the JavaScript client docs;
- `channel.typingEvents.enabled` in the client configuration, which defaults to `true`.

A server "no" wins. The resolved value is `channel.config.typingEvents.enabled`. When it's `false`, nothing is sent and the built-in indicators show nothing.

To turn typing events off in the client for every channel:

```ts
client.config.set({ channel: { typingEvents: { enabled: false } } });
```

To keep showing others' typing but stop sending it from some composers, change `text.publishTypingEvents` on those composers. This setup function turns it off for thread composers:

```ts
client.config.setSetupFunction("messageComposer", ({ composer }) => {
  if (composer.threadId) {
    composer.updateConfig({ text: { publishTypingEvents: false } });
  }
});
```

A user can also stop sharing their typing. With `privacy_settings.typing_indicators.enabled: false` on their user, the client sends no typing events and the server drops any that reach it:

```tsx
import { useCreateChatClient } from "stream-chat-react";

export const useClientWithoutTypingEvents = () =>
  useCreateChatClient({
    apiKey,
    tokenOrProvider: userToken,
    userData: {
      id: userId,
      privacy_settings: { typing_indicators: { enabled: false } },
    },
  });
```

[Configuration](https://getstream.io/chat/docs/sdk/react/v15/state/state-and-entities/configuration/) explains `client.config` and setup functions.

## Customizing

Replace the list's indicator through the `TypingIndicator` override key. It receives `isMessageListScrolledToBottom` and `scrollToBottom`, so it can stay hidden while the user reads older messages and keep the list at the bottom when it appears. [Typing in threads](#typing-in-threads) has a complete one.

`VirtualizedMessageList` renders whatever is registered under that key, and nothing when the key is empty. To show the default indicator there, register it:

```tsx
import type { Channel as ChannelType } from "stream-chat";
import {
  Channel,
  TypingIndicator,
  VirtualizedMessageList,
  WithComponents,
} from "stream-chat-react";

export const VirtualizedChannel = ({ channel }: { channel: ChannelType }) => (
  <WithComponents overrides={{ TypingIndicator }}>
    <Channel channel={channel}>
      <VirtualizedMessageList />
    </Channel>
  </WithComponents>
);
```

`TypingIndicatorHeader` is exported for custom headers. It shows typing in the channel, or in the current thread with `threadList`, which has the limit described in [Typing in threads](#typing-in-threads). The [Channel header](https://getstream.io/chat/docs/sdk/react/v15/recipes/headers/channel-header/) recipe builds a header around it.

The texts come from the `typing.singleUser`, `typing.twoUsers` and `typing.manyUsers` translation keys, see [Localization](https://getstream.io/chat/docs/sdk/react/v15/ui/customizing/localization/).

## Reference

- [Indicators](https://getstream.io/chat/docs/sdk/react/v15/component-reference/utilities/indicators/): the `TypingIndicator` props.
- [ComponentContext](https://getstream.io/chat/docs/sdk/react/v15/component-reference/channel/componentcontext/): the `TypingIndicator` override key.
- [Channel and channel state](https://getstream.io/chat/docs/sdk/react/v15/reference/state/channel-and-channel-state/): `channel.state.typing`, `keystroke` and `stopTyping`.
- [Configuration tree](https://getstream.io/chat/docs/sdk/react/v15/reference/state/configuration-tree/): `channel.typingEvents` and `messageComposer.text.publishTypingEvents`.

---

For the most recent version of this documentation, visit [https://getstream.io/chat/docs/sdk/react/v15/features/messaging/typing-indicators/](https://getstream.io/chat/docs/sdk/react/v15/features/messaging/typing-indicators/).