Info:
This is release candidate documentation for Stream Chat React SDK v15. For the latest stable version, see the latest version (v14).

Custom suggestions

The composer suggests items while the user types a trigger:

Trigger Suggests Example
@ people and groups @tom, @channel, @here, @admin, @Support Team
/ commands /giphy
: emoji, once you add it :smiling

This recipe changes how the suggestion list and its rows look, and adds the rows for a trigger of your own. Where the suggestions come from, and how to add a trigger, is explained in Suggestions. Emoji suggestions need the emoji middleware: see Emoji.

Enabling mention types

User mentions always work. @channel, @here, roles and user groups appear only for users with the matching capability, and roles and user groups also need data on the server. Mentions lists the capabilities and where to grant them. The composer checks them itself, so a row you replace never shows a mention the user can't make.

Best practices

  • Start from SuggestionList and defaultComponents, and replace only the rows you need.
  • Register the list through WithComponents with the AutocompleteSuggestionList key, so every composer below it uses it, in channels and threads alike.
  • Spread the props a row receives onto its button. They carry the click handler, the option role and the selection state that keyboard navigation relies on.
  • Keep rows light: the list re-renders them as the user types.
  • In a composer of your own, keep TextareaComposer. It opens the list, positions it and handles the arrow keys and Enter.

Customizing suggestion items

SuggestionList renders the open list. Its suggestionItemComponents prop maps each trigger to the component of a row, and defaultComponents holds the defaults for @, / and :.

The @ row is MentionItem. It renders a different row for each kind of mention, so to change only the people, keep MentionItem and replace its UserItem:

src/CustomSuggestionList.tsx
import type { MentionSuggestion } from "stream-chat";
import type {
  SuggestionListItemComponentProps,
  SuggestionListProps,
  UserItemProps,
} from "stream-chat-react";
import {
  Avatar,
  defaultComponents,
  MentionItem,
  SuggestionList,
} from "stream-chat-react";

const CustomUserItem = ({ entity, focused, ...buttonProps }: UserItemProps) => {
  const title = entity.name ?? entity.id;
  if (!title) return null;

  return (
    <button
      {...buttonProps}
      className={`suggestion-row ${focused ? "suggestion-row--focused" : ""}`}
      type="button"
    >
      <Avatar imageUrl={entity.image} size="sm" userName={title} />
      <span>{title}</span>
    </button>
  );
};

const CustomMentionItem = (props: SuggestionListItemComponentProps) => (
  <MentionItem
    {...props}
    entity={props.entity as MentionSuggestion}
    UserItem={CustomUserItem}
  />
);

const rows = { ...defaultComponents, "@": CustomMentionItem };

export const CustomSuggestionList = (props: SuggestionListProps) => (
  <SuggestionList {...props} suggestionItemComponents={rows} />
);
src/CustomSuggestionList.css
.suggestion-row {
  align-items: center;
  background: none;
  border: 0;
  display: flex;
  font: inherit;
  gap: 10px;
  padding: 8px 12px;
  width: 100%;
}

.suggestion-row--focused {
  background: #00000014;
}

Register the list once, around the app:

src/App.tsx
import type { PropsWithChildren } from "react";
import { WithComponents } from "stream-chat-react";
import { CustomSuggestionList } from "./CustomSuggestionList";
import "./CustomSuggestionList.css";

const overrides = { AutocompleteSuggestionList: CustomSuggestionList };

export const AppComponents = ({ children }: PropsWithChildren) => (
  <WithComponents overrides={overrides}>{children}</WithComponents>
);

Customizing rows per mention type

MentionItem reads entity.mentionType and renders the matching row. Replace any of them and the rest keep their defaults. Each row receives the same props: entity, narrowed to its kind of mention, focused, and the button props.

mentionType Prop of MentionItem Default row
user UserItem Avatar and name
channel / here BroadcastMentionItem @channel or @here with a description
role RoleItem The role's name and "Notify all … members"
user_group UserGroupItem The group's name and description
any other SpecialMentionItem Nothing

This one changes only the user group row:

src/GroupMentionItem.tsx
import type { MentionSuggestion } from "stream-chat";
import type {
  SuggestionListItemComponentProps,
  UserGroupItemProps,
} from "stream-chat-react";
import { MentionItem } from "stream-chat-react";

const CustomUserGroupItem = ({
  entity,
  focused,
  ...buttonProps
}: UserGroupItemProps) => (
  <button
    {...buttonProps}
    className={`suggestion-row ${focused ? "suggestion-row--focused" : ""}`}
    type="button"
  >
    👥 {entity.name}
  </button>
);

export const GroupMentionItem = (props: SuggestionListItemComponentProps) => (
  <MentionItem
    {...props}
    entity={props.entity as MentionSuggestion}
    UserGroupItem={CustomUserGroupItem}
  />
);

Use GroupMentionItem as the "@" entry of suggestionItemComponents, as in the example above.

Rows for a trigger of your own

A trigger of your own needs a row in suggestionItemComponents, under the trigger character. Without one, the list doesn't open. Custom triggers adds a # trigger that suggests tags, each { id, name }. This list renders its rows:

src/SuggestionListWithTags.tsx
import type {
  SuggestionListItemComponentProps,
  SuggestionListProps,
} from "stream-chat-react";
import { defaultComponents, SuggestionList } from "stream-chat-react";

type Tag = { id: string; name: string };

const TagItem = ({
  entity,
  focused,
  ...buttonProps
}: SuggestionListItemComponentProps) => (
  <button
    {...buttonProps}
    className={`suggestion-row ${focused ? "suggestion-row--focused" : ""}`}
    type="button"
  >
    #{(entity as Tag).name}
  </button>
);

const rows = { ...defaultComponents, "#": TagItem };

export const SuggestionListWithTags = (props: SuggestionListProps) => (
  <SuggestionList {...props} suggestionItemComponents={rows} />
);

Register SuggestionListWithTags under the AutocompleteSuggestionList key. Picking a tag runs the middleware's onSuggestionItemSelect, which writes it into the text.

Wrapping the default suggestion list

To add something around the list and keep its behaviour, wrap SuggestionList:

src/SuggestionListWithHeader.tsx
import type { SuggestionListProps } from "stream-chat-react";
import { SuggestionList } from "stream-chat-react";

export const SuggestionListWithHeader = (props: SuggestionListProps) => (
  <div className="suggestion-list-shell">
    <div className="suggestion-list-shell__header">Suggestions</div>
    <SuggestionList {...props} />
  </div>
);

Register it with the AutocompleteSuggestionList key, like the lists above.

Replacing AutocompleteSuggestionItem

Each row sits in a wrapper that handles selection: it scrolls the focused row into view and, on click, writes the item into the text and moves the focus back to the input. To change the wrapper for every trigger at once, override AutocompleteSuggestionItem. It receives SuggestionItemProps:

  • item, the suggestion;
  • focused, whether the row is the one the arrow keys reached;
  • component, the row component for the open trigger;
  • the button props it passes on to the row.

This one adds a class to every row and keeps the default wrapper:

src/CustomAutocompleteSuggestionItem.tsx
import type { SuggestionItemProps } from "stream-chat-react";
import { SuggestionListItem } from "stream-chat-react";

export const CustomAutocompleteSuggestionItem = (
  props: SuggestionItemProps,
) => <SuggestionListItem {...props} className="custom-suggestion-item" />;

Most apps need only AutocompleteSuggestionList and suggestionItemComponents. Override AutocompleteSuggestionItem when you change how a row is selected.

Custom inputs

A composer layout of your own keeps the suggestions as long as it renders TextareaComposer: the list, the keyboard navigation and its position next to the cursor come with it. The overrides on this page apply inside it too. The Custom composer recipe builds such a layout.