Custom suggestions

This recipe changes the composer's suggestion list, which opens while the user types a mention, a command or an emoji.

Note:

What you'll build

  • Compact mention rows with an avatar and a name
  • Rows of their own for user group mentions
  • Suggestions for a trigger of your own, such as # for tags
  • A wider suggestion box with rounder corners

Preview

The mention suggestions for @ shown as compact rows with an avatar and a name, the second row focused

Approach: Restyle and Replace one part. Choosing where to start explains the approaches.

Starts from: the base app. The page shows only what the recipe adds.

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

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, and keep the className it receives. They carry the click handler, the option role, the selection state that keyboard navigation relies on and the SDK's row classes.
  • 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 = ({
  className,
  entity,
  // Kept off the button: the list already gives the focused row its class.
  // eslint-disable-next-line @typescript-eslint/no-unused-vars
  focused,
  ...buttonProps
}: UserItemProps) => {
  const title = entity.name ?? entity.id;
  if (!title) return null;

  return (
    <button
      {...buttonProps}
      // Keeps the classes the list passes, such as the one for the focused row.
      className={`${className ?? ""} cookbook-suggestion-row`}
      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} />
);

The rules sit in the stream-overrides layer, so they win over the SDK's own rules once the SDK's stylesheet is in an earlier layer, as Theming → CSS layers sets up. They take their colors, spacing and type from the SDK's tokens, so they follow your theme.

The row keeps the className it receives and adds its own. The list passes the SDK's row classes through it, str-chat__suggestion-list-item and, on the focused row, str-chat__suggestion-list-item--selected, so styles and tests that rely on them keep working. The focused row's style hangs on that SDK class, so the row needs no modifier of its own, and focused stays off the button.

Register the list once, around ChatView:

src/WithCustomSuggestionList.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 WithCustomSuggestionList = ({ 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 = ({
  className,
  entity,
  // Kept off the button: the list already gives the focused row its class.
  // eslint-disable-next-line @typescript-eslint/no-unused-vars
  focused,
  ...buttonProps
}: UserGroupItemProps) => (
  <button
    {...buttonProps}
    className={`${className ?? ""} cookbook-suggestion-row`}
    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 = ({
  className,
  entity,
  // Kept off the button: the list already gives the focused row its class.
  // eslint-disable-next-line @typescript-eslint/no-unused-vars
  focused,
  ...buttonProps
}: SuggestionListItemComponentProps) => (
  <button
    {...buttonProps}
    className={`${className ?? ""} cookbook-suggestion-row`}
    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.

Styling the list's box

SuggestionList positions its own box next to the cursor. An element you wrap around it stays where the list is rendered, inside the composer, so a header or a frame placed around SuggestionList doesn't move with the list, and shows even while the list is closed. To change the box, pass a class with className. It goes on the box that holds the rows:

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

export const StyledSuggestionList = (props: SuggestionListProps) => (
  <SuggestionList {...props} className="cookbook-suggestion-list" />
);

The restyled list box

The mention suggestions in a wider box with rounder corners and a stronger shadow

Register it with the AutocompleteSuggestionList key, like the lists above. containerClassName puts a class on the positioned element around the box instead; leave its position to the list.

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="cookbook-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.

How it works

Info:

Example

The user types @b in Weekend trip, with WithCustomSuggestionList around ChatView:

  1. TextareaComposer sees the @ trigger. The composer finds the people that match b and opens the list registered under AutocompleteSuggestionList, CustomSuggestionList.
  2. SuggestionList takes the row for @ from suggestionItemComponents, CustomMentionItem. It renders MentionItem, which renders CustomUserItem for Bob: his avatar and his name.
  3. The user presses the down arrow. The list gives the focused row the class str-chat__suggestion-list-item--selected, and the CSS highlights it.
  4. The user presses Enter. The mention goes into the text, and the list closes.

Rows for / and : keep their defaults, from defaultComponents.