Principles
The way we build AI interfaces reflects what we believe about AI's role in products. These are taw-ui's convictions — not conventions. Some will feel obvious. Others will challenge how you've been building.
These aren't style guidelines. They're positions. We believe AI products that ignore interface quality are leaving the most important part of the experience on the floor.
The HAI Contract
Human-AI Interaction has three participants. Each has a clear responsibility. When any one oversteps, the experience breaks. The seven principles below are how this contract gets implemented in practice.
Returns structured data matching a schema. Introduces the component with context. References it later by ID in follow-up turns.
Describe what the component already shows. Make rendering decisions. Generate HTML or styles.
Validates data. Renders all lifecycle 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 output schemas. Maps tool names to components. Passes the part prop.
Write conditional rendering. Build loading states. Validate data manually. Style individual tool results.
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">Designed for the Interface Layer
AI interfaces have a different constraint set than traditional product UIs. They live inside a conversation flow. They appear at unpredictable moments. They must communicate their purpose immediately — without navigation, tabs, or a learning curve. taw-ui components are designed to satisfy these constraints first, not as an afterthought.
Chat is often where AI interfaces live today — but the principles here apply anywhere AI returns structured output: sidebars, command palettes, embedded panels, async notifications. The rules stay the same: single-column, immediately scannable, actionable with one interaction.
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?
These principles exist because we believe taw-ui has a job that matters. AI is getting faster. Outputs are getting richer. The interface layer is what decides whether that richness reaches users — or disappears into walls of text.
Build components that respect these principles, and your AI will feel smarter. Not because the model changed. Because the interface did.