# 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](https://getstream.io/docs-assets/images/7c0c7c1c288e.png)

---

**Approach:** Restyle and Replace one part. [Choosing where to start](https://getstream.io/chat/docs/sdk/react/ui-cookbooks/overview/#choosing-where-to-start) explains the approaches.

**Starts from:** [the base app](https://getstream.io/chat/docs/sdk/react/ui-cookbooks/overview/#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](https://getstream.io/chat/docs/sdk/react/state/writing-messages/suggestions/). Emoji suggestions need the emoji middleware: see [Emoji](https://getstream.io/chat/docs/sdk/react/features/rich-content/emoji/#autocomplete).

## 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](https://getstream.io/chat/docs/sdk/react/features/messaging/mentions/#turning-it-on-or-off) 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`:

```tsx label="React" title="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} />
);
```

```css label="CSS" title="src/CustomSuggestionList.css"
@layer stream-overrides {
  .cookbook-suggestion-row {
    display: flex;
    align-items: center;
    gap: var(--str-chat__spacing-sm);
    width: 100%;
    padding: var(--str-chat__spacing-xs) var(--str-chat__spacing-sm);
    border: 0;
    border-radius: var(--str-chat__radius-lg);
    background: none;
    color: var(--str-chat__text-primary);
    font: var(--str-chat__font-body-default);
    text-align: start;
    cursor: pointer;
  }

  /* The SDK marks the row the arrow keys reached with this class. */
  .cookbook-suggestion-row.str-chat__suggestion-list-item--selected {
    background: var(--str-chat__background-utility-selected);
  }
}
```

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](https://getstream.io/chat/docs/sdk/react/features/app-experience/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`:

```tsx title="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:

```tsx title="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](https://getstream.io/chat/docs/sdk/react/state/writing-messages/suggestions/#custom-triggers) adds a `#` trigger that suggests tags, each `{ id, name }`. This list renders its rows:

```tsx title="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:

```tsx label="React" title="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" />
);
```

```css label="CSS" title="src/StyledSuggestionList.css"
@layer stream-overrides {
  .cookbook-suggestion-list {
    min-width: 240px;
    padding: var(--str-chat__spacing-xs);
    border: 1px solid var(--str-chat__border-core-default);
    border-radius: var(--str-chat__radius-2xl);
    background: var(--str-chat__background-core-elevation-1);
    box-shadow: var(--str-chat__box-shadow-3);
  }
}
```

---

**The restyled list box**

![The mention suggestions in a wider box with rounder corners and a stronger shadow](https://getstream.io/docs-assets/images/92de0f82deda.png)

---

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:

```tsx title="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](https://getstream.io/chat/docs/sdk/react/ui-cookbooks/composer/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`.

## Related

- [Suggestions](https://getstream.io/chat/docs/sdk/react/state/writing-messages/suggestions/): where the suggestions come from, and how to add a trigger.
- [Mentions](https://getstream.io/chat/docs/sdk/react/features/messaging/mentions/): the mention types and the capabilities they need.
- [Emoji](https://getstream.io/chat/docs/sdk/react/features/rich-content/emoji/#autocomplete): the emoji middleware behind `:` suggestions.
- [Commands and Giphy](https://getstream.io/chat/docs/sdk/react/features/rich-content/commands-and-giphy/): the commands behind `/` suggestions.
- [Overriding components](https://getstream.io/chat/docs/sdk/react/ui/components/overriding-components/): how overrides registered with `WithComponents` apply.
- [Message composer UI](https://getstream.io/chat/docs/sdk/react/ui/components/message-composer-ui/): the composer's parts, `TextareaComposer` among them.
- [Custom composer](https://getstream.io/chat/docs/sdk/react/ui-cookbooks/composer/custom-composer/): a composer layout of your own that keeps the suggestions.

---

For the most recent version of this documentation, visit [https://getstream.io/chat/docs/sdk/react/ui-cookbooks/composer/custom-suggestions/](https://getstream.io/chat/docs/sdk/react/ui-cookbooks/composer/custom-suggestions/).