Message Composition Primitives
The custom renderer lets you take over how a message is drawn entirely. But most of the time what you want to change is the content, not the layout — full-width bot text, a right-aligned user bubble, the timestamp's format and theme color should all stay as they are.
So the SDK exports the primitives its default rows are built from, ready to compose.
Message Composition Primitives
To customize a message row's content without re-implementing the SDK's layout (full-width bot text, right-aligned user bubble, timestamp format and theme colors), compose the exported primitives: TemplateBox, TemplateBoxContent, BotMessageText, UserMessageText and Time. Use them from renderMessageContent and fall back with renderDefaultContent() for rows you don't handle.
import {
TemplateBox, TemplateBoxContent,
BotMessageText, UserMessageText, Time,
} from '@asgard-js/react';
<Chatbot
renderMessageContent={({ message, renderDefaultContent }) => {
if (message.type === 'user') {
return (
<TemplateBox type="user" direction="horizontal">
<UserMessageText>{/* your content */}</UserMessageText>
<Time time={message.time} />
</TemplateBox>
);
}
return renderDefaultContent();
}}
/>Both modes should look identical in layout — the only difference is that the composed mode turns the leading mention into a chip. That is the point of these primitives: change the content, keep the layout.
The Primitives
| Primitive | Role |
|---|---|
TemplateBox | The row frame — alignment and direction (type: 'user' | 'bot') |
TemplateBoxContent | The bot row's content container; handles quick replies and message actions |
BotMessageText | The bot text body, with Markdown rendering and theme colors |
UserMessageText | The user text bubble |
Time | The timestamp, formatted and themed via template.time.style |
Usage
Use them from renderMessageContent, and fall back with renderDefaultContent()
for rows you don't handle:
import {
TemplateBox,
TemplateBoxContent,
BotMessageText,
UserMessageText,
Time,
} from "@asgard-js/react";
import { MessageTemplateType } from "@asgard-js/core";
<Chatbot
renderMessageContent={({ message, renderDefaultContent }) => {
// User row: turn the leading mention into a chip, keep everything else
if (message.type === "user" && message.text.startsWith(MENTION)) {
return (
<TemplateBox type="user" direction="horizontal">
<UserMessageText>
<span className="mention-chip">{MENTION}</span>
{message.text.slice(MENTION.length)}
</UserMessageText>
<Time time={message.time} />
</TemplateBox>
);
}
// Bot text row
if (message.type === "bot") {
const template = message.message?.template;
const isText = template?.type === MessageTemplateType.TEXT || !template?.type;
const text = message.isTyping ? message.typingText : message.message?.text;
if (isText && text) {
return (
<TemplateBox type="bot" direction="horizontal">
<TemplateBoxContent
message={message}
quickReplies={template?.quickReplies}
references={template?.references}
>
<BotMessageText>{text}</BotMessageText>
</TemplateBoxContent>
</TemplateBox>
);
}
}
return renderDefaultContent();
}}
/>
Text While Streaming
When message.isTyping is true, read message.typingText rather than
message.message.text — the latter is only populated once the message completes. The
example above already handles this.
Why Not Just Hand-roll It
Hand-rolled rows drift from these, and keep drifting every time the SDK changes:
- Bot text is full-width with no bubble; only user rows are bubbles — different layout rules
- The timestamp's format and color come from the theme, not from hardcoded values
TemplateBoxContentalso positions quick replies and the message-action row
In short: change the content, keep the layout.
ChatHeader
The same idea applies to the heading bar. ChatHeader is exported for use with
renderHeader; to swap only the title area while keeping the rest of the structure,
use renderTitle. See Custom Header.
See Also
- Custom Renderer — take over a message entirely
- Message Actions — the action row under a message
- Custom Header — the heading bar's three customization tiers