# Reactions Customization

This example shows how to customize the SDK reaction surfaces for a channel subtree.

## Best Practices

- Keep custom reactions aligned with your product’s interaction model.
- Register shared reaction options through `WithComponents` so the selector and list stay in sync.
- Prefer the `{ quick, extended }` shape when you need different sets for the compact selector and expanded picker.
- Keep custom reaction handlers permission-aware and idempotent.
- Test custom reactions in both channel and thread message lists.
- If you replace the reactions detail dialog, decide whether to keep the built-in add-reaction step via `ReactionSelectorExtendedList`.

## Shared Reaction Options

`reactionOptions` lives in `ComponentContext`, so the usual override path is `WithComponents`.

```tsx
import { Channel, WithComponents } from "stream-chat-react";

const customReactionOptions = {
  quick: {
    arrow_down: {
      Component: () => <>⬇️</>,
      name: "Down vote",
    },
    arrow_up: {
      Component: () => <>⬆️</>,
      name: "Up vote",
    },
  },
  extended: {
    arrow_down: {
      Component: () => <>⬇️</>,
      name: "Down vote",
    },
    arrow_up: {
      Component: () => <>⬆️</>,
      name: "Up vote",
    },
    fire: {
      Component: () => <>🔥</>,
      name: "Fire",
    },
  },
};

export const WrappedChannel = ({ children }) => (
  <WithComponents overrides={{ reactionOptions: customReactionOptions }}>
    <Channel>{children}</Channel>
  </WithComponents>
);
```

![](https://getstream.io/docs-assets/images/0e4a8bf69461.png)

>
> **Note:** `MessageReactions` reads `reactionOptions` from `ComponentContext`. Keep reaction-option customization in `WithComponents` so `ReactionSelector`, `MessageReactions`, and `MessageReactionsDetail` stay aligned.
>

## Override The Default Reaction Surfaces

Use `WithComponents` to replace `ReactionSelector`, `MessageReactions`, or `MessageReactionsDetail`.

```tsx
import {
  Channel,
  MessageReactions,
  MessageReactionsDetail,
  ReactionSelector,
  WithComponents,
} from "stream-chat-react";

const CustomReactionSelector = (props) => <ReactionSelector {...props} />;

const CustomMessageReactions = (props) => (
  <MessageReactions {...props} visualStyle="segmented" />
);

const CustomMessageReactionsDetail = (props) => (
  <MessageReactionsDetail {...props} />
);
const CustomReactionSelectorExtendedList = (props) => (
  <ReactionSelector.ExtendedList {...props} />
);

export const WrappedChannel = ({ children }) => (
  <WithComponents
    overrides={{
      MessageReactionsDetail: CustomMessageReactionsDetail,
      ReactionSelectorExtendedList: CustomReactionSelectorExtendedList,
      ReactionSelector: CustomReactionSelector,
      MessageReactions: CustomMessageReactions,
    }}
  >
    <Channel>{children}</Channel>
  </WithComponents>
);
```

When you customize the detail view, keep the current all-reactions behavior in mind:

- `selectedReactionType={null}` means "show all reactions"
- clicking the active reaction filter again should reset it back to `null`
- when no reaction type is selected, the default detail view can show the emoji for each user row
- the add-reaction button inside the detail dialog opens `ReactionSelectorExtendedList`

If you want the default loading skeleton in a custom detail surface, reuse the exported loading indicator:

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

const CustomReactionsDetailLoadingState = () => (
  <MessageReactionsDetailLoadingIndicator />
);
```

If you need to handle very large reaction sets, note that the default `MessageReactions` trigger does not open the detail dialog once a message exceeds `1000` reactions. That guard lives in the stock component, so a fully custom `MessageReactions` override is the escape hatch.

Here's the difference between the two `visualStyle` options for `MessageReactions`:

| Clustered                                                       | Segmented                                                       |
| --------------------------------------------------------------- | --------------------------------------------------------------- |
| ![](https://getstream.io/docs-assets/images/af7172e91241.png) | ![](https://getstream.io/docs-assets/images/d155f82b4430.png) |

## Custom Reaction Handler

If you want to change behavior without replacing the whole selector UI, wrap `ReactionSelector` and override `handleReaction`.

```tsx
import { useCallback } from "react";
import {
  Channel,
  ReactionSelector,
  WithComponents,
  useChannelStateContext,
  useMessageContext,
} from "stream-chat-react";

const CustomReactionSelector = (props) => {
  const {
    message: { id: messageId, own_reactions: ownReactions = [] },
  } = useMessageContext("CustomReactionSelector");
  const { channel } = useChannelStateContext("CustomReactionSelector");

  const handleReaction = useCallback(
    async (reactionType, event) => {
      console.log({ event });

      const hasReactedWithType = ownReactions.some(
        (reaction) => reaction.type === reactionType,
      );

      if (hasReactedWithType) {
        await channel.deleteReaction(messageId, reactionType);
        return;
      }

      await channel.sendReaction(messageId, { type: reactionType });
    },
    [channel, messageId, ownReactions],
  );

  return <ReactionSelector {...props} handleReaction={handleReaction} />;
};

export const WrappedChannel = ({ children }) => (
  <WithComponents overrides={{ ReactionSelector: CustomReactionSelector }}>
    <Channel>{children}</Channel>
  </WithComponents>
);
```

## Read More

The reference page for the current reaction surfaces lives here:

- [Reactions](https://getstream.io/chat/docs/sdk/react/components/message-components/reactions/)

---

For the most recent version of this documentation, visit [https://getstream.io/chat/docs/sdk/react/guides/theming/reactions/](https://getstream.io/chat/docs/sdk/react/guides/theming/reactions/).