Cards
Send rich interactive cards with buttons, fields, and images across all platforms.
Cards let you send structured, interactive messages that render natively on each platform — Block Kit on Slack, Adaptive Cards on Teams, Discord embeds or Components, and Google Chat Cards.
Setup
Configure your tsconfig.json to use the Chat SDK JSX runtime:
{
"compilerOptions": {
"jsx": "react-jsx",
"jsxImportSource": "chat"
}
}Or use a per-file pragma:
/** @jsxImportSource chat */Basic card
import { Card, CardText, Button, Actions } from "chat";
await thread.post(
<Card title="Order #1234">
<CardText>Your order has been received!</CardText>
<Actions>
<Button id="approve" style="primary">Approve</Button>
<Button id="reject" style="danger">Reject</Button>
</Actions>
</Card>
);Components
Card
The top-level container. Accepts title and optional subtitle.
<Card title="My Card" subtitle="Optional subtitle">
{/* children */}
</Card>CardText
Renders formatted text. Supports a subset of markdown.
<CardText>**Bold** and _italic_ text</CardText>
<CardText style="bold">Bold section header</CardText>Use CardText instead of Text when using JSX to avoid conflicts with React's built-in types.
Section
Groups related content together.
<Section>
<CardText>Section content here</CardText>
</Section>Fields
Renders key-value pairs in a compact layout.
<Fields>
<Field label="Name" value="John Doe" />
<Field label="Role" value="Developer" />
<Field label="Team" value="Platform" />
</Fields>Button
An action button that triggers an onAction handler.
<Button id="approve" style="primary">Approve</Button>
<Button id="reject" style="danger">Reject</Button>
<Button id="details">View Details</Button>The id maps to your onAction handler. Optional value passes extra data:
<Button id="report" value="bug">Report Bug</Button>Set actionType="modal" to indicate the button opens a modal. The button still triggers your onAction handler, where you call event.openModal() — this prop tells adapters like Teams to wire up the button for dialog opening:
<Button id="open-feedback" actionType="modal">Give Feedback</Button>Optional callbackUrl causes the action data to be POSTed to a URL when clicked. See Callback URLs for details.
<Button callbackUrl={webhook.url} id="approve" style="primary">Approve</Button>CardLink
Inline hyperlink rendered as text. Unlike LinkButton (which must be inside Actions), CardLink can be placed directly in a card alongside other content.
<CardLink url="https://example.com/order/1234" label="View order details" />Or with children as the label:
<CardLink url="https://example.com/docs">Read the docs</CardLink>CardLink renders as a platform-native link: <url|label> on Slack, [label](url) on Teams/Discord/GitHub/Linear, and <a href> on Google Chat.
LinkButton
Opens an external URL. No onAction handler needed for navigation. On platforms
that emit link-button click events, such as Slack, pass id when you need a
stable action identifier for routing or analytics.
<LinkButton url="https://example.com/order/1234">View Order</LinkButton><LinkButton id="view_order" url="https://example.com/order/1234">
View Order
</LinkButton>Actions
Container for buttons and interactive elements.
<Actions>
<Button id="approve" style="primary">Approve</Button>
<Button id="reject" style="danger">Reject</Button>
<LinkButton url="https://example.com">View</LinkButton>
</Actions>Select
Inline dropdown menu.
<Actions>
<Select id="priority" label="Priority" placeholder="Select priority">
<SelectOption label="High" value="high" description="Urgent tasks" />
<SelectOption label="Medium" value="medium" />
<SelectOption label="Low" value="low" />
</Select>
</Actions>Selection triggers an onAction handler with the id as the actionId and the selected value.
RadioSelect
Radio button group for mutually exclusive choices.
<Actions>
<RadioSelect id="status" label="Status">
<SelectOption label="Open" value="open" />
<SelectOption label="In Progress" value="in_progress" />
<SelectOption label="Done" value="done" />
</RadioSelect>
</Actions>Table
Structured data display with column headers and rows. Renders as a native table on platforms that support it (Slack, Teams, GitHub, Linear), as GFM markdown in Discord card payloads, and as padded ASCII text elsewhere.
<Table
headers={["Name", "Age", "Role"]}
rows={[
["Alice", "30", "Engineer"],
["Bob", "25", "Designer"],
]}
/>Optional column alignment:
<Table
headers={["Name", "Amount"]}
rows={[["Alice", "$100"], ["Bob", "$200"]]}
align={["left", "right"]}
/>On Slack, tables render as paginated, sortable data tables. The optional caption (accessible table description) and pageSize (rows per page, 1–100) props tune that rendering and are ignored on other platforms:
<Table
headers={["Name", "Score"]}
rows={[["Alice", "98"], ["Bob", "87"]]}
caption="Quarterly review scores"
pageSize={10}
/>Chart
Data visualization with pie, bar, area, and line charts. Renders as a native data visualization on Slack; other platforms fall back to the chart's data rendered as a text table.
<Chart
title="My Favorite Candy Bars"
chart={{
type: "pie",
segments: [
{ label: "Kit Kat", value: 45 },
{ label: "Twix", value: 28 },
{ label: "Crunch", value: 18 },
],
}}
/>Bar, area, and line charts take named series plotted against shared categories:
<Chart
title="Daily Active Users"
chart={{
type: "line",
categories: ["Mon", "Tue", "Wed"],
xLabel: "Day",
yLabel: "Users",
series: [
{
name: "Web",
data: [
{ label: "Mon", value: 120 },
{ label: "Tue", value: 135 },
{ label: "Wed", value: 128 },
],
},
{
name: "Mobile",
data: [
{ label: "Mon", value: 80 },
{ label: "Tue", value: 95 },
{ label: "Wed", value: 90 },
],
},
],
}}
/>Slack enforces a 50-character title, up to 12 segments or series, up to 20 categories, 20-character labels, and at most 2 charts per message. Charts that exceed these limits fall back to a text rendering of the data instead of being rejected by the API.
Image
Embeds an image in the card.
<Image url="https://example.com/screenshot.png" alt="Screenshot" />Divider
A visual separator between sections.
<CardText>Above the line</CardText>
<Divider />
<CardText>Below the line</CardText>Full example
import {
Card, CardText, CardLink, Button, LinkButton, Actions,
Section, Fields, Field, Divider, Image,
Select, SelectOption, RadioSelect,
} from "chat";
await thread.post(
<Card title="User Profile" subtitle="Account details">
<Image url="https://example.com/avatar.png" alt="User avatar" />
<Fields>
<Field label="Name" value="Jane Smith" />
<Field label="Role" value="Engineer" />
<Field label="Team" value="Platform" />
</Fields>
<CardLink url="https://example.com/profile/123">View full profile</CardLink>
<Divider />
<Section>
<CardText>Select an action below to manage this profile.</CardText>
</Section>
<Actions>
<Select id="role" label="Change Role" placeholder="Select role">
<SelectOption label="Engineer" value="engineer" />
<SelectOption label="Manager" value="manager" />
<SelectOption label="Admin" value="admin" />
</Select>
<Button id="edit" style="primary">Edit Profile</Button>
<Button id="deactivate" style="danger">Deactivate</Button>
<LinkButton url="https://example.com/profile/123">View Full Profile</LinkButton>
</Actions>
</Card>
);