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
SuggestionListanddefaultComponents, and replace only the rows you need. - Register the list through
WithComponentswith theAutocompleteSuggestionListkey, 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
optionrole 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:
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} />
);.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:
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:
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:
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:
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:
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.