Message
An SDK-agnostic chat message with text, reasoning, tool calls, and sources
A compound chat message component. Sub-components take flat props — no SDK-specific types required. Works with Vercel AI SDK, Anthropic SDK, OpenAI SDK, LangChain, or any other provider. The consumer maps their SDK's output to sub-components.
Run demo
bunx @gridland/demo messagecurl -fsSL https://raw.githubusercontent.com/thoughtfulllc/gridland/main/scripts/run-demo.sh | bash -s messageInstallation
bunx shadcn@latest add @gridland/messageUsage
import { Message } from "@/components/ui/message"<Message role="user">
<Message.Content>
<Message.Text>Hello, can you help me?</Message.Text>
</Message.Content>
</Message>
<Message role="assistant">
<Message.Content>
<Message.Text>Of course! What do you need?</Message.Text>
</Message.Content>
<Message.Footer model="claude-opus-4-6" />
</Message>Examples
With PromptInput
Combine Message with PromptInput for a complete conversation view.
Streaming
Pass isStreaming to the message and isLast to the final text part to show a
streaming cursor.
<Message role="assistant" isStreaming>
<Message.Content>
<Message.Text isLast>{partialText}</Message.Text>
</Message.Content>
</Message>Tool Calls
Render tool calls with the generic 4-state model. Map your SDK's tool states
to pending, running, completed, or error.
<Message.ToolCall name="readFile" state="running" />
<Message.ToolCall name="search" state="completed" result="3 results found" />
<Message.ToolCall name="deploy" state="error" result="Permission denied" />Reasoning
Render a collapsible reasoning/thinking block above the content bubble.
<Message role="assistant">
<Message.Reasoning duration="1.2s" collapsed={false} />
<Message.Content>
<Message.Text>Here is the answer.</Message.Text>
</Message.Content>
</Message>Sources
Render numbered source citations.
<Message.Source title="API Reference" url="https://docs.example.com" index={0} />
<Message.Source title="Tutorial" url="https://example.com/guide" index={1} />Footer
Show model attribution and timestamp below the bubble.
<Message role="assistant">
<Message.Content>
<Message.Text>Here is the answer.</Message.Text>
</Message.Content>
<Message.Footer model="claude-opus-4-6" timestamp="just now" />
</Message>Mapping Vercel AI SDK Parts
Map message.parts from useChat to sub-components. The consumer owns the
mapping — the component has no SDK dependency.
import { useChat } from "@ai-sdk/react"
const { messages, status } = useChat({ api: "/api/chat" })
{messages.map((msg, i) => {
const isLast = i === messages.length - 1
const msgStreaming = isLast && msg.role === "assistant" && status === "streaming"
return (
<Message key={msg.id} role={msg.role} isStreaming={msgStreaming}>
<Message.Content>
{msg.parts?.map((part, j) => {
const isLastPart = j === msg.parts.length - 1
switch (part.type) {
case "text":
return <Message.Text key={j} isLast={isLastPart && msgStreaming}>{part.text}</Message.Text>
case "tool-invocation":
return (
<Message.ToolCall
key={j}
name={part.toolInvocation.toolName}
state={part.toolInvocation.state === "result" ? "completed" : "running"}
result={part.toolInvocation.result}
/>
)
case "source-url":
return <Message.Source key={j} title={part.title} url={part.url} index={j} />
default:
return null
}
})}
</Message.Content>
</Message>
)
})}Mapping Anthropic SDK
// Map Anthropic content blocks to sub-components
msg.content.map((block, j) => {
switch (block.type) {
case "text":
return <Message.Text key={j}>{block.text}</Message.Text>
case "thinking":
return <Message.Reasoning key={j} collapsed={false}>{block.thinking}</Message.Reasoning>
case "tool_use":
return <Message.ToolCall key={j} name={block.name} state="running" />
}
})Compound Components
All sub-components read shared state (role, streaming, background color) from
Message via context. No prop drilling needed.
| Component | Description |
|---|---|
Message.Content | Bubble wrapper with role-based background color |
Message.Text | Text with word wrap and optional streaming cursor |
Message.Reasoning | Collapsible Chain of Thought block |
Message.ToolCall | Tool call with status icon and result |
Message.Source | Numbered source citation |
Message.Footer | Model attribution and timestamp |
useMessage
Access message context from within any sub-component.
import { useMessage } from "@/components/ui/message"
function MyCustomStatus() {
const { role, isStreaming, textColor } = useMessage()
return <text>{role}: {isStreaming ? "streaming..." : "done"}</text>
}Tool Call States
The component uses 4 generic states that map to any SDK.
| State | Icon | Color | Use case |
|---|---|---|---|
pending | • | muted | Input being prepared, waiting to execute |
running | ⠋ | warning | Tool is executing |
completed | ✓ | success | Tool finished with result |
error | ✕ | error | Tool execution failed |
API Reference
Message
| Prop | Type | Default | Description |
|---|---|---|---|
role | "user" | "assistant" | "system" | - | Message role — determines alignment and background |
isStreaming | boolean | false | Show streaming cursor on the last text part |
streamingCursor | string | "▎" | Cursor character shown while streaming |
backgroundColor | string | - | Override the default role-based background color |
children | ReactNode | - | Compound sub-components |
Message.Text
| Prop | Type | Default | Description |
|---|---|---|---|
children | string | - | Text content |
isLast | boolean | false | Show the streaming cursor after this part |
Message.Reasoning
| Prop | Type | Default | Description |
|---|---|---|---|
duration | string | - | Duration label shown in the header |
steps | Step[] | - | Structured thinking steps |
collapsed | boolean | true | Whether the block starts collapsed |
children | ReactNode | - | Freeform content (used when steps are not provided) |
Message.ToolCall
| Prop | Type | Default | Description |
|---|---|---|---|
name | string | - | Tool name |
state | "pending" | "running" | "completed" | "error" | "pending" | Tool execution state |
result | unknown | - | Tool result (shown when completed or error) |
color | string | - | Override the default state color |
Message.Source
| Prop | Type | Description |
|---|---|---|
title | string? | Source title |
url | string? | Source URL |
index | number | Zero-based index for the citation number |
Message.Footer
| Prop | Type | Description |
|---|---|---|
model | string? | Model name |
timestamp | string? | Timestamp label |
MessageContextValue
interface MessageContextValue {
role: MessageRole
isStreaming: boolean
streamingCursor: string
backgroundColor: string
textColor: string
}Part Types (optional helpers)
The component exports optional type definitions for consumers who want a standard intermediate format. Sub-components do NOT depend on these types.
type MessagePart = TextPart | ReasoningPart | ToolCallPart | SourcePart| Type | Fields |
|---|---|
TextPart | type: "text", text: string |
ReasoningPart | type: "reasoning", text?, duration?, steps?, collapsed? |
ToolCallPart | type: "tool-call", name, state, args?, result? |
SourcePart | type: "source", title?, url? |