Principles
taw-ui is built for the next generation of Human-AI Interaction. These are the rules we follow — and the rules we think every AI tool UI library should follow.
The AI returns JSON. The component renders it. The developer writes zero glue code. That's the entire contract.
The Schema Is the API
One Zod schema is the single source of truth. It validates on the server, validates on the client, infers TypeScript types, and drives rendering. There are no separate action definitions, surface configurations, or render instructions.
The AI doesn't need to know about React. It doesn't need to know about components. It returns data that matches a schema — the same schema it already uses as its tool's outputSchema. The rendering is the framework's job, not the model's job.
// Server: define tool output shape
const getMetrics = tool({
outputSchema: KpiCardSchema,
execute: async () => ({
id: "revenue",
label: "Revenue",
value: 142580,
confidence: 0.92,
}),
})
// Client: render it
<KpiCard part={part} />Zero Glue Code
Components handle their entire lifecycle from a single prop. Loading? Skeleton with shimmer. Streaming? Progressive render. Success? Animated entrance. Parse error? Helpful inline error with field-level suggestions. The developer never writes conditional rendering for tool states.
// Handles loading, streaming, error, AND success
<KpiCard part={part} />
// Not this:
if (part.state === "loading") return <Skeleton />
if (part.state === "error") return <Error msg={part.error} />
const data = validate(part.output)
if (!data) return <ParseError />
return <KpiCard data={data} />Common affordances are built in, not opt-in. A DataTable sorts columns, formats currencies, and shows row counts by default. A KpiCard animates numbers and shows confidence badges by default. You don't configure these — you disable them if you don't want them.
AI-Native Fields
Every schema supports two fields that don't exist in traditional UI libraries: confidence (0–1) and source (provenance). These are first-class because AI outputs are probabilistic — users deserve to know how certain the data is and where it came from.
| Confidence | Badge | Signal |
|---|---|---|
| ≥ 0.8 | Green | High certainty — safe to act on |
| 0.5 – 0.8 | Amber | Moderate — verify before acting |
| < 0.5 | Red | Low certainty — treat as estimate |
These fields are optional in every schema. If the AI doesn't provide them, the component renders cleanly without them. But when they're present, the user sees exactly how much to trust the output. No other library does this.
Decisions Produce Receipts
When a component asks the user to decide (approve, select, confirm), the interactive controls must transform into a permanent record after the decision. This is the receipt pattern. It prevents stale buttons in chat history, saves vertical space, and gives the AI a clear signal of what was chosen.
const [receipt, setReceipt] = useState<TawReceipt>()
<OptionList
part={part}
onAction={(actionId, payload) => {
// payload.receipt is ready to use
setReceipt(payload.receipt)
}}
receipt={receipt}
/>
// Before decision: full interactive list
// After decision: compact "✓ Rolling Deploy" receiptReceipt labels use past tense: Approved, Selected, Confirmed, Skipped. The action description stays imperative. Pattern: [Past-tense verb] [What was acted on]
Everything Is Addressable
Every component, every option, every row has a stable id. These must be backend identifiers (database IDs, slugs, URLs) — never array indexes or random UUIDs generated at render time.
Addressability is what makes the AI useful after the first response. When the user says " tell me more about the Acme Corp row", the AI can reference row.name === "Acme Corp" because the data has stable keys. When the user picks an option, the receipt contains selectedIds: ["rolling"] — not selectedIndex: 0.
<div data-taw-component="kpi-card" data-taw-id="revenue-q4">Chat-Native by Default
AI tool UIs live inside chat. Chat is a vertical feed, 400–600px wide, where attention is scarce. Every component must communicate its purpose within the first 300px of vertical space. If a component needs tabs, navigation, or horizontal scrolling to be understood — it's too complex.
No input fields. The chat composer is the input. The only interactive elements are selection (pick from options the AI provides) and confirmation (approve/reject what the AI proposes). Limit visible choices to 5–7. If the AI needs to show more, it should say so — not render a paginated table.
Fail Helpfully, Never Silently
When the AI returns data that doesn't match the schema, the component must never render null, crash, or show a blank space. It renders a helpful inline error that shows exactly which fields failed validation and suggests corrections.
Parse errors show the expected type, the received value, and a" Did you mean? " suggestion when possible. This is critical for development — the AI model iterates faster when it can see what went wrong. And in production, a visible error is always better than a silent one.
missing label (string) — received field "title"
missing value (number | string) — received field "amount"
Did you mean: label → title, value → amount?
The HAI Contract
Human-AI Interaction has three participants. Each has a clear responsibility. When one oversteps, the experience breaks.
Returns structured data matching a schema. Introduces the component with context. References it later by ID.
Describe what the component already shows. Make rendering decisions. Generate HTML or styles.
Validates data. Renders all states. Provides built-in affordances. Produces receipts from decisions.
Fetch data. Make AI calls. Render outside its container. Assume context about the conversation.
Registers tools with schemas. Maps tool names to components. Passes the part prop.
Write conditional rendering. Build loading states. Validate data manually. Style individual tool results.