import { MessageList } from "stream-chat-react";
const customRenderText = (text) => {
return <span>{text}</span>;
};
export const WrappedMessageList = () => (
<MessageList renderText={customRenderText} />
);renderText function
renderText controls how message text is formatted. By default it parses Markdown and returns a ReactElement using ReactMarkdown from react-markdown, with the remark parser and remark/rehype plugins.
The default remark plugins used by SDK are:
remark-gfm- a third party plugin to add GitHub-like markdown support
The default rehype plugins (both specific to this SDK) are:
- plugin to render mentions
- plugin to render emojis
Best Practices
- Keep Markdown enabled unless your product requires strict plain text.
- Sanitize or restrict allowed tags when rendering untrusted content.
- Add custom plugins sparingly to avoid performance regressions.
- Keep mention and emoji rendering consistent across message views.
- Test custom renderers with long messages and mixed formatting.
Overriding Defaults
Custom renderText Function
If you don’t want Markdown support, pass a custom renderText function that returns a React node. You can provide it to MessageList or MessageSimple via the renderText prop.
For example, here’s a minimal version that wraps text in a <span>:
Here’s an example with VirtualizedMessageList, which doesn’t accept renderText directly:
import { VirtualizedMessageList, MessageSimple } from "stream-chat-react";
const customRenderText = (text) => {
return <span>{text}</span>;
};
const CustomMessage = (props) => (
<MessageSimple {...props} renderText={customRenderText} />
);
export const WrappedVirtualizedMessageList = () => (
<VirtualizedMessageList Message={CustomMessage} />
);Custom Element Rendering
If you like the default output but want to customize how specific ReactMarkdown components render (for example, **strong**), pass options as the third argument to renderText:
mention and emoji are special component types generated by the SDK’s custom rehype plugins. Mention nodes expose mentionedEntity; direct user mentions also expose the backwards-compatible mentionedUser.
import { renderText } from "stream-chat-react";
const CustomStrongComponent = ({ children }) => (
<b className="custom-strong-class-name">{children}</b>
);
const CustomMentionComponent = ({ children, node: { mentionedEntity } }) => {
if (mentionedEntity.mentionType !== "user") {
return (
<span data-mention-type={mentionedEntity.mentionType}>{children}</span>
);
}
return (
<a
data-user-id={mentionedEntity.id}
href={`/user-profile/${mentionedEntity.id}`}
>
{children}
</a>
);
};
export const WrappedMessageList = () => (
<MessageList
renderText={(text, mentionedUsers, options) =>
renderText(text, mentionedUsers, {
...options,
customMarkDownRenderers: {
strong: CustomStrongComponent,
mention: CustomMentionComponent,
},
})
}
/>
);Styling Mentions Per Type
If you only want different styling per mention type (not different markup), you don't need a custom mention component. The default renderer outputs a <span> you can target with CSS:
<span
class="str-chat__message-mention"
data-mention-type="user | channel | here | role | user_group"
data-mention-id="<entity id>"
data-user-id="<user id, only for user mentions>"
>@Support Team</span
>Use the data-mention-type attribute to color each variant independently:
.str-chat__message-mention[data-mention-type="channel"],
.str-chat__message-mention[data-mention-type="here"] {
color: #e62e2e;
}
.str-chat__message-mention[data-mention-type="role"] {
color: #9c27b0;
}
.str-chat__message-mention[data-mention-type="user_group"] {
color: #0c8d3a;
}Reach for the custom mention component (shown above) only when a type needs different markup or behavior — for example wrapping user mentions in a profile link while leaving the others as plain spans.
If you build mention metadata yourself via options.messageMentionEntities, you can drop the mention types a channel isn't allowed to use before rendering with the exported filterRenderTextMentionEntitiesByChannelCapabilities(entities, channelCapabilities) helper. See Enabling mention types.
Custom Remark and Rehype Plugins
To extend the plugin list, provide custom remark/rehype arrays via getRemarkPlugins and getRehypePlugins. These receive the default arrays, and you return the final list. Pass them as the third renderText parameter.
To understand remark/rehype plugins, start with react-remark, which is used under the hood.
import { renderText, RenderTextPluginConfigurator } from "stream-chat-react";
import { customRehypePlugin } from "./rehypePlugins";
import { customRemarkPlugin } from "./remarkPlugins";
const getRehypePlugins: RenderTextPluginConfigurator = (plugins) => {
return [customRehypePlugin, ...plugins];
};
const getRemarkPlugins: RenderTextPluginConfigurator = (plugins) => {
return [customRemarkPlugin, ...plugins];
};
const customRenderText = (text, mentionedUsers, options) =>
renderText(text, mentionedUsers, {
...options,
getRehypePlugins,
getRemarkPlugins,
});
const WrappedMessageList = () => <MessageList renderText={customRenderText} />;You can also define a custom set of allowed tag names. For tree transforms, use libraries like unist-builder plus unist-util-visit or hast-util-find-and-replace:
import { findAndReplace } from "hast-util-find-and-replace";
import { u } from "unist-builder";
import {
defaultAllowedTagNames,
renderText,
RenderTextPluginConfigurator,
} from "stream-chat-react";
// wraps every letter b in <xxx></xxx> tags
const customTagName = "xxx";
const replace = (match) =>
u("element", { tagName: customTagName }, [u("text", match)]);
const customRehypePlugin = () => (tree) => findAndReplace(tree, /b/, replace);
const getRehypePlugins: RenderTextPluginConfigurator = (plugins) => {
return [customRehypePlugin, ...plugins];
};
const customRenderText = (text, mentionedUsers, options) =>
renderText(text, mentionedUsers, {
...options,
allowedTagNames: [...defaultAllowedTagNames, customTagName],
getRehypePlugins,
});
const WrappedMessageList = () => <MessageList renderText={customRenderText} />;Existing Remark and Rehype Plugins
The SDK already exports the following plugins:
Remark plugins
htmlToTextPlugin: Treats HTML nodes found in the Markdown as plain text (no HTML is parsed or executed). Useful for preventing inline HTML from rendering.imageToLink: Converts Markdown images () into links (<a href="url">label</a>). The link text defaults to the image URL, but can be configured to use the imagealtortitle.keepLineBreaksPlugin: Preserves multiple blank source lines by inserting<br>elements between block nodes so visual spacing matches the original input.plusPlusToEmphasis: Adds support for “inserted” text using++text++, which renders as<ins>text</ins>. Skips code, math, and links.remarkIgnoreMarkdown: Disables Markdown parsing entirely and renders the original input as plain text inside a single paragraph.
Rehype plugins
mentionsMarkdownPlugin(mentionEntities): Detects mention display text based on the provided mention entities and wraps matches in a custom<mention>element with amentionedEntitypayload. It also accepts the olderUserResponse[]argument for direct user mentions only. Handles e-mail-like display names that GFM splits intomailto:links.emojiMarkdownPlugin: Wraps native Unicode emoji in a custom<emoji>element, enabling consistent styling/behavior for emoji in rendered output.
We recommend to pass mention metadata through options.messageMentionEntities when calling renderText. The second renderText argument, mentionedUsers, is deprecated and only supports direct user mentions.
import { renderText } from "stream-chat-react";
const customRenderText = (text, mentionedUsers, options) =>
renderText(text, mentionedUsers, {
...options,
messageMentionEntities: [
...(options?.messageMentionEntities ?? []),
{ id: "channel", mentionType: "channel", name: "channel" },
{ id: "admin", mentionType: "role", name: "admin" },
],
});To select which plugins to apply:
import {
keepLineBreaksPlugin,
MessageList,
renderText,
useChannelStateContext,
} from "stream-chat-react";
const customRenderText = (text?: string, mentionedUsers?: UserResponse[]) =>
renderText(text, mentionedUsers, {
getRemarkPlugins: () => [keepLineBreaksPlugin],
// disabled all the SDK's rehype plugins
getRehypePlugins: () => [],
});
const WrappedMessageList = () => <MessageList renderText={customRenderText} />;