# Typing indicators

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.
- **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 [The indicator in the virtualized list](#the-indicator-in-the-virtualized-list).
- **Layout.** Not involved.

[What channels and threads hold](https://getstream.io/chat/docs/sdk/react/state/channels-and-threads/what-channels-and-threads-hold/) explains the rest of `channel.state`. [Channel UI](https://getstream.io/chat/docs/sdk/react/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

The default indicators filter `channel.state.typing` by `parent_id`, so each one shows the typing of the list it sits in: the channel's, or one thread's. [Typing indicator](https://getstream.io/chat/docs/sdk/react/ui/components/channel-ui/#typing-indicator) in Channel UI shows what each indicator displays in a `<Channel/>` and in a `<Thread/>`.

The `typing.stop` the client sends by itself after a pause carries no `parent_id`. The channel removes the user's entry either way.

## 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, set `text.publishTypingEvents` to `false` on those composers with a `messageComposer` setup function. [Setup functions](https://getstream.io/chat/docs/sdk/react/state/state-and-entities/configuration/#setup-functions) shows one that turns it off for thread composers.

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/state/state-and-entities/configuration/) explains `client.config` and setup functions.

## Customizing

### A typing indicator of your own

Register a component under the `TypingIndicator` key. It receives `isMessageListScrolledToBottom` and `scrollToBottom`: hide it while the user reads older messages, and scroll down when it appears. This `TextTypingIndicator` shows the names as plain text:

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

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

export const TextTypingIndicator = ({
  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)
    // Inside a thread, its 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>
  );
};

const overrides = { TypingIndicator: TextTypingIndicator };

// Wrap <Channel/> and <Thread/> in it, so both message lists use the indicator.
export const WithTextTypingIndicator = ({ children }: PropsWithChildren) => (
  <WithComponents overrides={overrides}>{children}</WithComponents>
);
```

### The indicator in the virtualized list

`<VirtualizedMessageList/>` shows a typing indicator only when one is registered under the `TypingIndicator` key. To show the default one, 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>
);
```

>
> **Note:** Don't register it for a livestream or another channel with many messages and members. There, typing events add traffic for every member, and the indicator rarely shows anything readable. [Turning off typing, presence and delivery receipts](https://getstream.io/chat/docs/sdk/react/features/channels/livestream-chat/#turning-off-typing-presence-and-delivery-receipts) in Livestream chat turns them off.
>

### The indicator in a header of your own

`<TypingIndicatorHeader/>` is exported for custom headers. It shows typing in the channel. With its `threadList` prop set, it shows typing in the thread around it instead. The [Channel header](https://getstream.io/chat/docs/sdk/react/ui-cookbooks/headers/channel-header/) and [Thread header](https://getstream.io/chat/docs/sdk/react/ui-cookbooks/headers/thread-header/) recipes build headers around it.

### The texts

Both indicators take their text from these translation keys:

| Key                                              | Default text                                                    | Shown when                                                  |
| ------------------------------------------------ | --------------------------------------------------------------- | ----------------------------------------------------------- |
| `typing.singleUser`                              | `{{ typing }} is typing`                                        | one person is typing                                        |
| `typing.twoUsers`                                | `{{ firstUser }} and {{ secondUser }} are typing`               | two people are typing                                       |
| `typing.manyUsers_one`, `typing.manyUsers_other` | `{{ count }} person is typing`, `{{ count }} people are typing` | three or more people are typing, or one of them has no name |

`typing.twoUsers` gets each name as its own value, `firstUser` and `secondUser`, so a translation words the whole sentence, including the word between the names. A German dictionary, for example:

```tsx
import { Chat, Streami18n } from "stream-chat-react";

const streamI18n = new Streami18n({ language: "de" });

streamI18n.registerTranslation("de", {
  "typing.twoUsers": "{{ firstUser }} und {{ secondUser }} schreiben",
});

export const App = () => (
  <Chat client={client} i18nInstance={streamI18n}>
    {/* the rest of the app */}
  </Chat>
);
```

[Localization](https://getstream.io/chat/docs/sdk/react/features/app-experience/localization/) explains how to register dictionaries and change the other texts.

## Reference

- [Indicators](https://getstream.io/chat/docs/sdk/react/component-reference/utilities/indicators/): the `TypingIndicator` props.
- [ComponentContext](https://getstream.io/chat/docs/sdk/react/component-reference/channel/componentcontext/): the `TypingIndicator` override key.
- [Channel and channel state](https://getstream.io/chat/docs/sdk/react/reference/state/channel-and-channel-state/): `channel.state.typing`, `keystroke` and `stopTyping`.
- [Configuration tree](https://getstream.io/chat/docs/sdk/react/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/features/messaging/typing-indicators/](https://getstream.io/chat/docs/sdk/react/features/messaging/typing-indicators/).