Custom Autocomplete Suggestion List

Autocomplete suggestion list components can be customized via WithComponents:

  • AutoCompleteSuggestionHeader
  • AutoCompleteSuggestionItem
  • AutoCompleteSuggestionList

Best Practices

  • Preserve default components for triggers you don't customize to keep expected behavior.
  • Keep list rendering lightweight; suggestion updates happen on every keystroke.
  • Use keyboardShouldPersistTaps="always" in custom lists to avoid dismissing the keyboard.
  • Normalize triggerType mapping (/, :, @) to avoid mismatched UI.
  • Ensure onSelect updates the composer state rather than mutating input directly.

Header and item components are used inside the list's FlatList. These props let you customize the header, items, or the entire list.

The available suggestion trigger types are:

  • command for /
  • emoji for :
  • mention for @

You can render custom UI for specific triggers and fall back to defaults for the rest.

The mention trigger surfaces a typed MentionSuggestion union from stream-chat with five variants discriminated by item.mentionType:

mentionTypeTokenNotifies
user@<name>A single user
channel@channelEveryone in the channel
here@hereMembers currently online
role@<role>Users with that role
user_group@<group>Members of a named user group

The default AutoCompleteSuggestionItem dispatches each variant to its own row component. See Customizing per-mention-type rows to override one or more variants while keeping the SDK defaults for the rest.

Enabling enhanced mentions on the dashboard

@<user> mentions work out of the box. The broadcast (@channel, @here), role (@<role>), and user-group (@<group>) variants require the matching permission to be granted to the sender's role on the channel type. Until the permissions are enabled, the variants are filtered out of the suggestion list returned by the API and the rendered tokens fall back to plain text.

Open the Stream Dashboard, pick your app, then navigate to Chat Messaging → Roles & Permissions. Select the role you want to grant the capability to (for example channel_member) and the scope of the channel type that should allow it (for example messaging), then enable the relevant permissions:

PermissionEnables
NotifyChannel@channel
NotifyHere@here
NotifyRole@<role>
NotifyGroup@<group>

Stream Dashboard — Roles & Permissions page with the NotifyChannel, NotifyHere, NotifyRole and NotifyGroup permissions enabled for the channel_member role in the messaging scope

User group mentions additionally require user groups to be defined on the app via the user groups API. Role mentions resolve against the roles defined under Chat Messaging → Roles & Permissions in the same dashboard.

Customizing Header component

The header renders at the top of the list and is customized via AutoCompleteSuggestionHeader.

The props for the component are:

  • triggerType: The trigger type of the suggestion list.
  • queryText: The value to display in the header.

An example for the same would be as follows:

import {
  AutoCompleteSuggestionHeader,
  WithComponents,
} from "stream-chat-react-native";
import { Text } from "react-native";

const CustomAutoCompleteSuggestionHeader = ({ queryText, triggerType }) => {
  if (triggerType === "command") {
    return <Text>Command Header Component</Text>;
  } else if (triggerType === "emoji") {
    return <Text>Emoji Header Component</Text>;
  } else {
    return (
      <AutoCompleteSuggestionHeader
        queryText={queryText}
        triggerType={triggerType}
      />
    );
  }
};

<WithComponents
  overrides={{
    AutoCompleteSuggestionHeader: CustomAutoCompleteSuggestionHeader,
  }}
>
  <Channel channel={channel}>{/* The underlying components */}</Channel>
</WithComponents>;

Note: Return the default AutoCompleteSuggestionHeader to keep default behavior for a trigger.

Customizing Item Component

Customize list items via the AutoCompleteSuggestionItem override in WithComponents.

The props for the component are as follows:

  • triggerType: The trigger type of the suggestion list.
  • itemProps: Varies by trigger. command has name and args, emoji is an Emoji, and mention is a MentionSuggestion union (from stream-chat) — switch on itemProps.mentionType to distinguish user, channel, here, role, and user_group variants.
import {
  Avatar,
  AutoCompleteSuggestionItem,
  WithComponents,
} from "stream-chat-react-native";
import { Text, View } from "react-native";

const CustomAutoCompleteSuggestionItem = ({ itemProps, triggerType }) => {
  if (triggerType === "command") {
    return (
      <View>
        <Text>{itemProps.name}</Text>
        <Text>{itemProps.args}</Text>
      </View>
    );
  } else if (triggerType === "mention") {
    const { id, image, name, online } = itemProps;
    return (
      <View>
        <Avatar image={image} name={name} online={online} size={30} />
        <Text>{itemProps.name}</Text>
      </View>
    );
  } else {
    return (
      <AutoCompleteSuggestionItem
        itemProps={itemProps}
        triggerType={triggerType}
      />
    );
  }
};

<WithComponents
  overrides={{ AutoCompleteSuggestionItem: CustomAutoCompleteSuggestionItem }}
>
  <Channel channel={channel}>{/*The underlying components*/}</Channel>
</WithComponents>;

Note: Return the default AutoCompleteSuggestionItem to keep default behavior for a trigger.

Customizing per mention type rows

For the mention trigger, the SDK ships a built-in dispatcher (MentionSuggestionItem) that switches on item.mentionType and renders one of four built-in row components:

mentionTypeDefault componentRenders
userMentionUserItemAvatar + tokenized display name (matched part bold)
channel / hereMentionBroadcastItemMegaphone icon + @channel / @here + description
roleMentionRoleItemShield icon + role name + "Notify all … members"
user_groupMentionUserGroupItemPeople icon + group name + optional description

To swap just one variant while delegating the rest to the SDK defaults, override AutoCompleteSuggestionItem and pass any unmodified variants through. The variant components and their underlying building blocks are exported from stream-chat-react-native:

import {
  AutoCompleteSuggestionItem,
  EnhancedMentionContent,
  EnhancedMentionIcon,
  MentionItem,
  WithComponents,
} from "stream-chat-react-native";

const MyBroadcastRow = ({ itemProps }) => (
  <MentionItem
    leading={
      <EnhancedMentionIcon
        Icon={({ height, width }) => (
          <Text style={{ height, width, textAlign: "center" }}>📣</Text>
        )}
      />
    }
  >
    <EnhancedMentionContent
      title={itemProps.mentionType === "channel" ? "@channel" : "@here"}
      subtitle={
        itemProps.mentionType === "channel"
          ? "Notify everyone in this channel"
          : "Notify every online member"
      }
    />
  </MentionItem>
);

const CustomAutoCompleteSuggestionItem = ({ itemProps, triggerType }) => {
  if (
    triggerType === "mention" &&
    (itemProps.mentionType === "channel" || itemProps.mentionType === "here")
  ) {
    return <MyBroadcastRow itemProps={itemProps} />;
  }

  return (
    <AutoCompleteSuggestionItem
      itemProps={itemProps}
      triggerType={triggerType}
    />
  );
};

<WithComponents
  overrides={{ AutoCompleteSuggestionItem: CustomAutoCompleteSuggestionItem }}
>
  <Channel channel={channel}>{/* The underlying components */}</Channel>
</WithComponents>;

Falling through to AutoCompleteSuggestionItem for everything else keeps the SDK defaults for the trigger types and mention variants you don't touch — you only need to handle the variants you actually want to override.

The building blocks you can compose with — all exported from stream-chat-react-native:

  • MentionItem — the row layout primitive ({ leading?, children, testID? }). Use it as the outer wrapper to inherit the default tap target and spacing.
  • EnhancedMentionContent — title + optional subtitle pair ({ title, subtitle? }).
  • EnhancedMentionIcon — circular icon chip wrapping any IconProps-shaped component ({ Icon, size?, color? }).
  • TokenizedSuggestionParts — renders a tokenized display name and bolds the substring that matches the current query ({ tokenizedDisplayName?, fallback? }). Useful inside a custom user row.

Theming mention rows

Mention rows pick up styles from the messageComposer.suggestions.mention subtree:

messageComposer: {
  suggestions: {
    mention: {
      avatarSize: number;
      column: ViewStyle;
      container: ViewStyle;
      // Applied to the user mention row (`MentionUserItem`)
      name: TextStyle;
      tag: TextStyle;
      // Applied to broadcast/role/user_group rows
      enhancedMentionContainer: ViewStyle;
      enhancedMentionIcon: ViewStyle;
      enhancedMentionTitle: TextStyle;
      enhancedMentionSubtitle: TextStyle;
    }
  }
}

name and tag still drive user-mention typography — they are kept separate from enhancedMention* so user-mention rows and broadcast/role/group rows can be themed independently.

const themeStyle = {
  messageComposer: {
    suggestions: {
      mention: {
        enhancedMentionTitle: {
          fontWeight: "600",
          fontSize: 16,
        },
        enhancedMentionSubtitle: {
          color: "#7a7a7a",
        },
      },
    },
  },
};

<Chat style={themeStyle}>{/* ... */}</Chat>;

Theming rendered mention text

When a sent message is rendered, every mention token (@<user>, @channel, @here, @<role>, @<group>) is colored using one of four per-type semantic tokens:

TokenUsed for
chatTextMentionUser@<user> mentions
chatTextMentionBroadcast@channel and @here
chatTextMentionRole@<role> mentions
chatTextMentionGroup@<group> mentions

Matching background tokens (chatBgMentionUser, chatBgMentionBroadcast, chatBgMentionRole, chatBgMentionGroup) are available for any UI that wants a background swatch per mention type.

All per-type tokens fall back to the umbrella chatTextMention / chatBgMention by default, so existing themes look identical until you opt in to per-type coloring. Override them via the semantics block of your theme:

const themeStyle = {
  semantics: {
    chatTextMentionBroadcast: "#e62e2e",
    chatTextMentionRole: "#9c27b0",
    chatTextMentionGroup: "#0c8d3a",
  },
};

<Chat style={themeStyle}>{/* ... */}</Chat>;

Pressing a rendered mention fires onPressMessage with emitter: "textMention" and an additionalInfo.mentionedEntity that carries the same mentionType discriminator. See Custom message press handler for the payload shape.

Customizing the AutoComplete suggestion list

You can also replace the entire list via the AutoCompleteSuggestionList override in WithComponents.

The props available to the component are:

  • AutoCompleteSuggestionHeader - The header component to be used in the list.
  • AutoCompleteSuggestionItem - The item component to be used in the list.

Example:

import {
  AutoCompleteSuggestionHeader,
  AutoCompleteSuggestionItem,
  useMessageComposer,
  useStateStore,
  WithComponents,
} from "stream-chat-react-native";
import { FlatList, Text, View } from "react-native";
import { TextComposerState, SearchSourceState } from "stream-chat";

const textComposerStateSelector = (state: TextComposerState) => ({
  suggestions: state.suggestions,
  text: state.text,
});

const searchSourceStateSelector = (nextValue: SearchSourceState) => ({
  items: nextValue.items,
});

const CustomAutoCompleteSuggestionList = () => {
  const messageComposer = useMessageComposer();
  const { textComposer } = messageComposer;
  const { suggestions } = useStateStore(
    textComposer.state,
    textComposerStateSelector,
  );
  const { items } =
    useStateStore(suggestions?.searchSource.state, searchSourceStateSelector) ??
    {};
  const trigger = suggestions?.trigger;
  const queryText = suggestions?.query;

  const triggerType = {
    "/": "command",
    ":": "emoji",
    "@": "mention",
  }[trigger ?? ""];

  const onSelect = (item) => {
    // Handle the selection of the item
    console.log("Selected item:", item);
  };

  if (triggerType === "command") {
    return (
      <View>
        <View>
          <Text>Command suggestions for {queryText}</Text>
        </View>
        {data.map((item) => (
          <AutoCompleteSuggestionItem
            itemProps={item}
            key={item.name}
            triggerType={triggerType}
          />
        ))}
      </View>
    );
  } else if (triggerType === "emoji") {
    return (
      <FlatList
        data={data}
        keyboardShouldPersistTaps="always"
        ListHeaderComponent={
          <AutoCompleteSuggestionHeader
            queryText={queryText}
            triggerType={triggerType}
          />
        }
        renderItem={({ index, item }) => (
          <TouchableOpacity
            onPress={() => {
              onSelect(item);
            }}
          >
            <Text>{item.unicode}</Text>
          </TouchableOpacity>
        )}
      />
    );
  } else {
    return (
      <View>
        <AutoCompleteSuggestionHeader
          queryText={queryText}
          triggerType={triggerType}
        />
        {data.map((item) => (
          <AutoCompleteSuggestionItem
            itemProps={item}
            key={item.name}
            triggerType={triggerType}
          />
        ))}
      </View>
    );
  }
};

<WithComponents
  overrides={{ AutoCompleteSuggestionList: CustomAutoCompleteSuggestionList }}
>
  <Channel channel={channel}>{/*The underlying components*/}</Channel>
</WithComponents>;

Note: Return the default AutoCompleteSuggestionList to keep default behavior for a trigger.