Building Your First Generative UI with Vercel AI SDK
Step-by-step guide to creating your first AI-powered interface with streaming components.
Prerequisites
Before we start, make sure you have:
- Node.js 18+ installed
- A Next.js 14+ project using the App Router
- An OpenAI API key (or Anthropic — the SDK supports both)
- Basic familiarity with React Server Components
If you are new to RSC, spend 15 minutes with the Next.js docs on Server Components first. The Vercel AI SDK's streamUI function depends on RSC and becomes much clearer once you understand the model.
What We're Building
We will build a simple AI-powered assistant that generates interactive UI based on user prompts. By the end of this tutorial, you will have a working Generative UI feature that:
- Takes a text prompt from the user
- Streams React components back from the server
- Renders interactive cards and charts based on the AI's decisions
The example domain is a financial assistant that can show stock prices and weather data — simple enough to understand quickly, complex enough to demonstrate real patterns.
⚠️ AI SDK RSC and
streamUIare marked experimental by Vercel. For production projects Vercel recommends AI SDK UI (useChatfrom@ai-sdk/react). This article shows a working RSC streaming pattern for prototypes, demos, and controlled environments; for production, weigh the trade-offs and see «When Vercel AI SDK Is NOT Your Pick». Migration guide RSC → UI.
Step 1: Install Dependencies
npm install ai@^4 @ai-sdk/openai@^1 zod
Pin v4 — the last series with the RSC API in the parameters: shape and ai/rsc import. For v5+, see the note on differences at the end of the article.
The ai package is the Vercel AI SDK core. @ai-sdk/openai is the OpenAI provider (swap in @ai-sdk/anthropic if you prefer Claude). zod handles tool parameter validation — it is how you define what parameters the AI can pass to each component.
Add your API key to .env.local:
OPENAI_API_KEY=sk-...
Step 2: Create Your Component Library
Define the components the AI can generate. These are regular React components — nothing AI-specific about them. The key design principle: build components that are useful standalone, and they will be composable by the AI.
// components/weather-card.tsx
interface WeatherCardProps {
city: string;
temperature: number;
conditions: string;
humidity: number;
}
export function WeatherCard({ city, temperature, conditions, humidity }: WeatherCardProps) {
return (
<div className="rounded-lg border bg-card p-6 shadow-sm">
<h3 className="text-lg font-semibold">{city}</h3>
<div className="mt-2 flex items-baseline gap-2">
<span className="text-4xl font-bold">{temperature}°C</span>
<span className="text-muted-foreground">{conditions}</span>
</div>
<p className="mt-2 text-sm text-muted-foreground">
Humidity: {humidity}%
</p>
</div>
);
}
// components/stock-ticker.tsx
interface StockTickerProps {
symbol: string;
price: number;
change: number;
changePercent: number;
}
export function StockTicker({ symbol, price, change, changePercent }: StockTickerProps) {
const isPositive = change >= 0;
const sign = isPositive ? '+' : '';
const color = isPositive ? 'text-green-600' : 'text-red-600';
return (
<div className="rounded-lg border bg-card p-6 shadow-sm">
<div className="flex items-center justify-between">
<h3 className="text-xl font-bold">{symbol}</h3>
<span className={`text-sm font-medium ${color}`}>
{sign}{changePercent.toFixed(2)}%
</span>
</div>
<div className="mt-2 flex items-baseline gap-2">
<span className="text-3xl font-bold">${price.toFixed(2)}</span>
<span className={`text-sm ${color}`}>
{sign}{change.toFixed(2)} today
</span>
</div>
</div>
);
}
// components/loading-skeleton.tsx
export function CardSkeleton({ height = 'h-32' }: { height?: string }) {
return (
<div className={`animate-pulse rounded-lg bg-muted ${height} w-full`} />
);
}
Step 3: Define AI Tools (Server Action)
This is the core of Generative UI. Create a server action that connects your components to the AI as "tools" — functions the model can decide to call:
// app/actions.tsx
'use server';
export const runtime = 'edge';
import { streamUI } from 'ai/rsc';
import { openai } from '@ai-sdk/openai';
import { z } from 'zod';
import { WeatherCard } from '@/components/weather-card';
import { StockTicker } from '@/components/stock-ticker';
import { CardSkeleton } from '@/components/loading-skeleton';
export async function generateUI(prompt: string) {
const result = await streamUI({
model: openai('gpt-4o'),
system: `You are a helpful financial and information assistant.
Use the available tools to display information visually
whenever possible. Prefer showing components over text responses.
When asked about weather or stocks, always use the appropriate tool.`,
prompt,
tools: {
showWeather: {
description: 'Display current weather conditions for a city. Use this when the user asks about weather, temperature, or climate.',
parameters: z.object({
city: z.string().describe('The city name, e.g. "Paris" or "New York"'),
temperature: z.number().describe('Current temperature in Celsius'),
conditions: z.string().describe('Weather description, e.g. "Partly cloudy"'),
humidity: z.number().min(0).max(100).describe('Relative humidity percentage'),
}),
generate: async function* (params) {
// Yield a skeleton immediately while data "loads"
yield <CardSkeleton height="h-36" />;
// In a real app, you would fetch live weather data here
return <WeatherCard {...params} />;
},
},
showStock: {
description: 'Display a stock price and daily change. Use this when the user asks about stock prices, market data, or a company\'s shares.',
parameters: z.object({
symbol: z.string().describe('Stock ticker symbol, e.g. "AAPL" or "TSLA"'),
price: z.number().describe('Current stock price in USD'),
change: z.number().describe('Price change today in USD'),
changePercent: z.number().describe('Percentage price change today'),
}),
generate: async function* (params) {
yield <CardSkeleton height="h-32" />;
return <StockTicker {...params} />;
},
},
},
});
return result.value;
}
Three things worth understanding about this code:
The generate function is an async generator. The yield keyword sends the skeleton immediately — before the AI finishes resolving parameters. The return sends the final component. This is how streaming Generative UI works.
Tool descriptions are instructions to the AI. The description fields are what the model reads to decide which tool to call. Write them clearly, including when the tool should and should not be used.
Zod schemas enforce the contract. The AI cannot pass invalid parameters if you define strict Zod schemas. Validation failures are caught before the component renders.
Step 4: Build the UI
// app/page.tsx
'use client';
import { useState } from 'react';
import { generateUI } from './actions';
const EXAMPLE_PROMPTS = [
"What's the weather like in Tokyo?",
"Show me Apple's current stock price",
"Compare the weather in London and New York",
"How is Tesla stock doing?",
];
export default function Home() {
const [prompt, setPrompt] = useState('');
const [messages, setMessages] = useState<Array<{ prompt: string; ui: React.ReactNode }>>([]);
const [loading, setLoading] = useState(false);
async function handleSubmit(e: React.FormEvent) {
e.preventDefault();
if (!prompt.trim() || loading) return;
const currentPrompt = prompt;
setPrompt('');
setLoading(true);
const ui = await generateUI(currentPrompt);
setMessages(prev => [...prev, { prompt: currentPrompt, ui }]);
setLoading(false);
}
return (
<main className="mx-auto max-w-2xl p-8">
<h1 className="text-3xl font-bold">Generative UI Demo</h1>
<p className="mt-2 text-muted-foreground">
Ask about weather or stocks — watch the AI generate the right interface.
</p>
{/* Example prompts */}
<div className="mt-4 flex flex-wrap gap-2">
{EXAMPLE_PROMPTS.map(p => (
<button
key={p}
onClick={() => setPrompt(p)}
className="rounded-full border px-3 py-1 text-sm hover:bg-muted"
>
{p}
</button>
))}
</div>
{/* Prompt input */}
<form onSubmit={handleSubmit} className="mt-6 flex gap-2">
<input
value={prompt}
onChange={e => setPrompt(e.target.value)}
placeholder="Ask anything..."
className="flex-1 rounded-md border bg-background px-4 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-ring"
/>
<button
type="submit"
disabled={loading || !prompt.trim()}
className="rounded-md bg-primary px-4 py-2 text-sm text-primary-foreground disabled:opacity-50"
>
{loading ? 'Generating...' : 'Ask'}
</button>
</form>
{/* Generated UI output */}
<div className="mt-8 space-y-6">
{messages.map((msg, i) => (
<div key={i}>
<p className="mb-2 text-sm font-medium text-muted-foreground">
"{msg.prompt}"
</p>
{msg.ui}
</div>
))}
</div>
</main>
);
}
Step 5: Run and Test
npm run dev
Try these prompts in order to see different behaviors:
- "What's the weather in Paris?" — single WeatherCard
- "Show me Apple stock" — single StockTicker
- "Compare the weather in London and New York" — the AI calls
showWeathertwice, generating two cards side by side - "How's Tesla doing and what's the weather in San Francisco?" — the AI calls both tools, generating mixed component types
That third prompt is the key demonstration: without any additional code, the model composes multiple components to answer a multi-part question.
What's Happening Under the Hood
When you submit a prompt:
- The client calls the
generateUIserver action streamUIsends the prompt + tool definitions to the OpenAI API- The model chooses which tools to call and with what parameters
- Each tool's
generatefunction immediately yields a skeleton - The AI finishes resolving parameters, and the final component is returned
- React renders the component in place of the skeleton
The RSC streaming protocol is what makes this work. The server serializes React component trees and streams them to the client incrementally. This is different from a JSON API — the client receives rendered components, not raw data.
Error Handling
Generated components can fail in ways hand-coded components do not. But there's a subtlety worth flagging: React error boundaries only catch render-time errors. A stream failure (network drop, OpenAI timeout, server-side tool error) will not be caught by the boundary — you need to handle that explicitly in handleSubmit.
Defend in two layers — try/catch around the stream, and an error boundary around the rendered UI:
// components/genui-error-boundary.tsx
'use client';
import { Component, ReactNode } from 'react';
interface Props { children: ReactNode; fallback?: ReactNode }
interface State { hasError: boolean; error: Error | null }
export class GenUIErrorBoundary extends Component<Props, State> {
constructor(props: Props) {
super(props);
this.state = { hasError: false, error: null };
}
static getDerivedStateFromError(error: Error) {
return { hasError: true, error };
}
render() {
if (this.state.hasError) {
return this.props.fallback ?? (
<div className="rounded-lg border border-destructive/50 bg-destructive/5 p-4">
<p className="text-sm text-destructive">
This component could not render. The AI may have passed unexpected data.
</p>
</div>
);
}
return this.props.children;
}
}
And catch stream errors on the client side:
async function handleSubmit(e: React.FormEvent) {
e.preventDefault();
if (!prompt.trim() || loading) return;
const currentPrompt = prompt;
setPrompt('');
setLoading(true);
try {
const ui = await generateUI(currentPrompt);
setMessages(prev => [...prev, { prompt: currentPrompt, ui }]);
} catch (err) {
// Network errors, OpenAI timeouts, server action failures — all land here
setMessages(prev => [...prev, {
prompt: currentPrompt,
ui: <div className="text-sm text-destructive">Stream failed. Try again.</div>,
}]);
} finally {
setLoading(false);
}
}
Wrap the generated UI with GenUIErrorBoundary on the page — it handles render errors, and the try/catch handles everything else.
When NOT to Use Vercel AI SDK
The SDK is solid, but it isn't a silver bullet. Skip it if:
- The SDK is marked experimental — documented limitations: streams cannot be aborted from server actions, components remount on
.done()(flicker), many<Suspense>boundaries can crash the page,createStreamableUIproduces quadratic transfer volume. For production Vercel recommends AI SDK UI. - You're not on Next.js.
streamUIis built on React Server Components, which require Next.js App Router (or Waku / another RSC-aware framework). For Vite SPAs, Remix without RSC, Vue, Svelte, or Angular — see the alternatives below. - You need a fixed UI with dynamic data. If the interface is known up front and the LLM only fills in data, use plain
generateObject+ your static React. Generative UI pays off when AI decides which components to show. - Strict privacy or on-prem deployment. The SDK assumes hosted providers (OpenAI, Anthropic). For self-hosted LLMs, a thin layer over
vLLM/Ollamaplus your own component registry is simpler. - Real-time collaboration or multiplayer. The RSC stream is one-way. For two-way UI sync between users you want WebSocket-based solutions, not RSC.
- Token budget is critical. Every render is an LLM call. Past MAU > 10k without aggressive caching, gpt-4o costs can clear $1k/month.
Alternatives for Non-Next.js Projects
| Tool | Stack | When to pick it |
|---|---|---|
| Thesys C1 API | Any (HTTP API) | SaaS that returns ready-to-render UI blocks via JSON schema. Great for teams without RSC expertise. |
| CopilotKit | React (Next.js + Vite + Remix) | In-app copilots with state and actions. Supports Generative UI via useCopilotAction. |
| Tambo | React (universal) | Component catalog as a first-class concept. Works on Vite, no RSC required. |
| A2UI | Any (Google) | Google's declarative JSON UI format for agents. Renderer-agnostic, paints on any frontend. |
| assistant-ui | React | Chat-first library with tool-UI support. Solid foundation for copilots on any React app. |
| Roll your own | Any | If you need 2–3 component types and control matters — registry + generateObject + a client-side switch is ~150 lines. |
For Vue / Svelte / Angular as of May 2026, there is no production-grade equivalent to Vercel AI SDK. Most teams ship a thin client to an API that returns a JSON component description and render it themselves.
Deployment on Low-Cost Platforms
Vercel is the obvious choice for Next.js, but not the only one. If budget matters or you want to avoid vendor lock-in:
- Fly.io — $0–5/month on hobby plans. Next.js via Dockerfile, edge regions worldwide. Free tier caps at 3 machines × 256MB.
- Render — free web services sleep after 15 minutes of inactivity (first request after sleep takes ~30s). Fine for demos and pet projects, not for production.
- Railway — $5 in monthly credits on the hobby plan. Simple GitHub-driven deploys, excellent DX, but more expensive than Fly.io as you scale.
- Cloudflare Pages + Workers — free up to 100k requests/day. Needs
nodejs_compatruntime, RSC streaming works with caveats. - Your own VPS + Coolify / Dokploy — from $5/month (Hetzner, Contabo). Full control, you own updates, SSL, monitoring.
For most pet projects Fly.io strikes the best balance: free tier to start, a real production path, edge regions, no vendor lock-in.
Team Skill Requirements
Before you pull Vercel AI SDK into production, assess team and stack readiness:
Required skills:
- React Server Components — without this,
streamUIis a black box at the first bug. - TypeScript — Zod schemas and tool parameters degrade into mud without types.
- Async generators (
async function*,yield) — not every mid-level React engineer has used these. - Prompt engineering — tool descriptions and the system prompt define component-selection quality. It's a separate discipline.
Nice-to-have skills:
- LLM API experience (rate limits, retry strategies, token accounting).
- Edge runtime familiarity and its limits (no Node.js APIs, bundle-size cap).
- Observability — structured logs of tool calls, request tracing.
Team size and TCO (rough, May 2026):
| Size | Engineering time | LLM cost (MAU 1k) | LLM cost (MAU 10k) | Realistic? |
|---|---|---|---|---|
| Solo (1) | 2–3 weeks to MVP | ~$50/mo (gpt-4o-mini) | ~$500/mo | Yes, side-project sweet spot |
| Small (2–4) | 4–6 weeks to v1 | ~$150/mo (gpt-4o mix) | ~$1.5k/mo | Yes, primary use case |
| Mid (5–15) | 2–3 months to full integration | ~$300/mo | ~$3k–5k/mo | Yes, if a platform exists |
| Large (15+) | 4–6 months + platform team | budget negotiated | $10k+/mo | Worth evaluating self-hosted LLM |
LLM numbers assume "1 request per session, gpt-4o for tool selection, gpt-4o-mini for parameters." Real costs depend heavily on prompt length, retry rate, and caching strategy.
TCO methodology: figures computed under these assumptions — average prompt ~800 input + ~300 output tokens on gpt-4o (or ~$0.001 on gpt-4o-mini), 1 request/session, OpenAI pricing as of 2026-05, MAU ≈ DAU × 30%. Calibrate to your own workload.
Deployment Tips
Environment variables: OPENAI_API_KEY must be available in your production environment. On Vercel, add it in project settings under Environment Variables.
Edge runtime: The streamUI function works on Edge runtime, which reduces cold start times significantly. Add export const runtime = 'edge' to your server action file.
Rate limiting: Without rate limiting, a single user could generate thousands of AI requests. Add a rate limiter before the streamUI call. The @upstash/ratelimit package integrates well with Next.js.
Model selection: gpt-4o produces the best component selections but costs more. gpt-4o-mini is roughly 15× cheaper on input/output (openai.com/api/pricing, 2026-05) and works well for simple component sets. Test both with your specific tool definitions.
Next Steps
This tutorial covered the fundamentals. For production Generative UI:
- Add more tools — each new component you add to the registry expands what the AI can answer
- Implement tool result caching — cache common queries to reduce latency and cost
- Add streaming text alongside UI components so the AI can explain what it is showing
- Use structured outputs for more reliable parameter generation
- Set up observability — log every tool call, its parameters, and user interactions
The Vercel AI SDK documentation covers all of these patterns in depth, and the examples repository has production-grade starter templates worth studying.
On AI SDK v5/v6
If you're using newer SDK versions, the key differences from the code in this article:
parameters:in the tool definition →inputSchema:import { streamUI } from 'ai/rsc'→import { streamUI } from '@ai-sdk/rsc'- RSC is still marked experimental by Vercel — for production, AI SDK UI (
useChat) is recommended.
Want to implement Generative UI in your product? Let's discuss your use case — in our consulting experience, the GenUI stack fits dashboards and internal tools well; for regulated surfaces and high-traffic public pages the trade-offs usually don't add up.
Disclosure: external product links (Thesys, CopilotKit, Tambo, Vercel, Fly.io, Render, Railway, Cloudflare, Upstash) are organic recommendations; no affiliate programs, no paid placements. Prices accurate as of 2026-05-11.
Alex
Generative UI Engineer & Consultant
Senior engineer specializing in AI-powered interfaces and Generative UI systems. Helping product teams ship faster with the right GenUI stack.
Related Articles
Κατασκευάζοντας το Πρώτο σας Generative UI με το Vercel AI SDK
Βήμα-βήμα οδηγός για τη δημιουργία της πρώτης σας AI-powered διεπαφής με streaming συστατικά.
Προσβασιμότητα σε Generative UI: Δημιουργία Συμπεριληπτικών AI Διεπαφών
Πρακτικός οδηγός για προσβάσιμα γεννητικά interfaces — screen readers, πλοήγηση με πληκτρολόγιο και συνδυαστικά προβλήματα προσβασιμότητας.
CopilotKit vs Vercel AI SDK vs Thesys: Σύγκριση Frameworks
Μια ειλικρινής σύγκριση των τριών κύριων frameworks Generative UI, με πλεονεκτήματα, μειονεκτήματα και πότε να χρησιμοποιείτε το καθένα.
Stay ahead on Generative UI
Weekly articles, framework updates, and practical implementation guides — straight to your inbox.
Need help implementing what you just read?
Book a Free Consultation