| IBM | International Business Machines | $170.42 | +1.12↑ | +0.66% | 18M |
| AAPL | Apple | $178.25 | +2.35↑ | +1.34% | 52M |
| MSFT | Microsoft | $380.00 | +1.24↑ | +0.33% | 31M |
| INTC | Intel Corporation | $39.85 | -0.42↓ | -1.04% | 30M |
| ORCL | Oracle Corporation | $110.31 | +0.78↑ | +0.71% | 14M |
<DataTable columns={[ { "key": "symbol", "label": "Symbol", "priority": "primary" }, { "key": "name", "label": "Company", "priority": "primary" }, { "key": "price", "label": "Price", "align": "right", "priority": "primary", "format": { "kind": "currency", "currency": "USD", "decimals": 2 } }, { "key": "change", "label": "Change", "align": "right", "priority": "secondary", "format": { "kind": "delta", "decimals": 2, "upIsPositive": true, "showSign": true } }, { "key": "changePercent", "label": "Change %", "align": "right", "priority": "secondary", "format": { "kind": "percent", "decimals": 2, "showSign": true, "basis": "unit" } }, { "key": "volume", "label": "Volume", "align": "right", "priority": "secondary", "format": { "kind": "number", "compact": true } } ]} data={[ { "symbol": "IBM", "name": "International Business Machines", "price": 170.42, "change": 1.12, "changePercent": 0.66, "volume": 18420000 }, { "symbol": "AAPL", "name": "Apple", "price": 178.25, "change": 2.35, "changePercent": 1.34, "volume": 52430000 }, { "symbol": "MSFT", "name": "Microsoft", "price": 380, "change": 1.24, "changePercent": 0.33, "volume": 31250000 }, { "symbol": "INTC", "name": "Intel Corporation", "price": 39.85, "change": -0.42, "changePercent": -1.04, "volume": 29840000 }, { "symbol": "ORCL", "name": "Oracle Corporation", "price": 110.31, "change": 0.78, "changePercent": 0.71, "volume": 14230000 } ]} rowIdKey="symbol"/>Search results, issue lists, transaction logs: the assistant surfaces tabular data constantly. DataTable renders those rows as a sortable table on desktop and stacked cards on mobile. You define columns with declarative formatting configs for currency, dates, status badges, and links. No raw JSON, no custom table from scratch.
Role: Information. For displaying data the user reads. See Design Guidelines for how component roles work.
Run this once from your project root.
npx shadcn@latest add @tool-ui/data-tableRender DataTable in your UI with tool-compatible props.
import { DataTable, type Column } from "@/components/tool-ui/data-table";type Row = { id: string; issue: string; priority: "high" | "medium" | "low"; createdAt: string; amount: number;};const columns: Column<Row>[] = [ { key: "issue", label: "Issue", priority: "primary", truncate: true }, { key: "priority", label: "Urgency", format: { kind: "status", statusMap: { high: { tone: "danger", label: "High" }, medium: { tone: "warning", label: "Medium" }, low: { tone: "neutral", label: "Low" }, }, }, }, { key: "createdAt", label: "Created", format: { kind: "date", dateFormat: "relative" }, }, { key: "amount", label: "Amount", align: "right", format: { kind: "currency", currency: "USD", decimals: 2 }, },];const data: Row[] = [ { id: "1", issue: "Payment failing on checkout", priority: "high", createdAt: "2025-11-11T09:24:00Z", amount: 129.99, }, { id: "2", issue: "Billing UI alignment bug", priority: "medium", createdAt: "2025-11-10T17:03:00Z", amount: 42, }, { id: "3", issue: "Export CSV missing headers", priority: "low", createdAt: "2025-11-08T08:00:00Z", amount: 0, }, { id: "4", issue: "Webhook retries inconsistent", priority: "high", createdAt: "2025-11-13T22:15:00Z", amount: 527.5, },];export default function Example() { return ( <DataTable rowIdKey="id" columns={columns} data={data} defaultSort={{ by: "createdAt", direction: "desc" }} /> );}Register this renderer so tool results display as DataTable.
// Backend toolimport { tool, jsonSchema } from "ai";const searchProducts = tool({ description: "Search products and return results in a table", inputSchema: jsonSchema<{ query: string }>({ type: "object", properties: { query: { type: "string" } }, required: ["query"], additionalProperties: false, }), async execute({ query }) { const results = await db.products.search(query); return { id: "data-table-search-products", columns: [ { key: "name", label: "Product", priority: "primary" }, { key: "price", label: "Price", format: { kind: "currency", currency: "USD" }, }, { key: "stock", label: "Stock", align: "right" }, ], data: results.map((p) => ({ id: p.id, name: p.name, price: p.price, stock: p.stock, })), }; },});// Frontend with assistant-uiimport { type Toolkit } from "@assistant-ui/react";import { DataTable } from "@/components/tool-ui/data-table";import { safeParseSerializableDataTable } from "@/components/tool-ui/data-table/schema";import { createResultToolRenderer } from "@/components/tool-ui/shared";export const toolkit: Toolkit = { searchProducts: { type: "backend", render: createResultToolRenderer({ safeParse: safeParseSerializableDataTable, render: (parsedResult) => ( <> <DataTable rowIdKey="id" {...parsedResult} /> </> ), }), },};Table on desktop, stacked cards on mobile
Tri-state column headers: none, ascending, descending
Currency, dates, links, badges, and status pills via column config
Compose action buttons below the table
Use explicit components when you want to force a layout:
<DataTable> (default): responsive auto layout<DataTable.Table>: always render the table view<DataTable.Cards>: always render the card viewimport { DataTable } from "@/components/tool-ui/data-table";Prop
Type
Prop
Type
Prop
Type
Use ToolUI.LocalActions to compose surface-level actions below DataTable.
All visual formatting for data stays declarative via columns[i].format. Keep row values as primitives and describe presentation through format configs.
Prop
Type
<table> markup with proper header associationsaria-live for assistive technologies