AI EngineeringGenerative UIReactStreamingVercel AI SDK

Generative UI: Interfaces that Design Themselves

U
UX Engineer
Featured Guide 20 min read

Hardcoded Forms are Legacy.

Why show a "Credit Card" form if the user wants to pay with Crypto? Why show a generic dashboard if the user asks "How are my sales in Japan?"

Generative UI means the LLM decides which components to render and how to configure them, in real-time.

02. It's Not Just Text

Using React Server Components (RSC), we can stream a serialized React Component tree from the server to the client. The LLM acts as the router and the prop-builder.

Deep Dive: The Wire Format

RSC payload looks like JSON:
1:I["./app/components/StockCard.tsx", "client", "default"]
It's safe. We aren't injecting raw HTML strings (XSS risk). We are injecting Component References that Hydrate on the client.

// actions.tsx
export async function
getResponse(input: string) {'{'}
  
const
ui =
await
streamUI(model, {'{'}
    message: input,
    tools: {'{'}
      showStockPrice: {'{'}
        generate: (symbol) => <StockCard symbol={symbol} />
      {'}'}
    {'}'}
  {'}'});
  
return
ui;
{'}'}

03. Accessibility Challenges

Dynamic UI is a nightmare for Screen Readers if you aren't careful. Since the user doesn't know what will appear, you must ensure every generated component announces itself.

Rule: Enforce aria-label in your Zod schemas so the LLM must generate a description.

04. The Senior Engineer's Take

Design Systems are Key.

You cannot let the AI generate raw HTML. It will look terrible and break accessibility.

Instead, feed the AI a strict library of your own high-quality components (Cards, Tables, Charts). The AI becomes the Assembler, not the Designer.

Interactive Playground

import React, { useState } from 'react';

// ✨ Generative UI Visualizer

export default function GenUIDemo() {
    const [prompt, setPrompt] = useState("");
    const [uiState, setUiState] = useState({ type: 'empty', data: null });
    const [isGenerating, setIsGenerating] = useState(false);

    const prompts = [
        "Show me sales for Q1 2026",
        "I need to schedule a meeting with potential investors",
        "My credit card expired, update it"
    ];

    const handleSubmit = async (text) => {
        setIsGenerating(true);
        setPrompt(text);
        
        // Simulate LLM latency
        await new Promise(r => setTimeout(r, 1200));

        // Logic (Simulated LLM Decision)
        if (text.toLowerCase().includes("sales")) {
            setUiState({ type: 'chart', data: 'Sales Data' });
        } else if (text.toLowerCase().includes("schedule")) {
            setUiState({ type: 'calendar', data: 'Calendar' });
        } else if (text.toLowerCase().includes("credit")) {
            setUiState({ type: 'payment', data: 'Payment Form' });
        } else {
            setUiState({ type: 'text', data: "I didn't understand that context, but here is a generic response." });
        }
        
        setIsGenerating(false);
    };

    return (
        <div className="bg-slate-50 dark:bg-slate-950 p-8 rounded-3xl border border-slate-200 dark:border-slate-800 shadow-xl h-[600px] flex flex-col">
             <div className="flex justify-between items-center mb-6">
                <h3 className="text-2xl font-black text-gray-900 dark:text-white flex items-center gap-3">
                    <span className="text-pink-500"></span> Generative Interface
                </h3>
            </div>

            {/* Canvas Area */}
            <div className="flex-1 bg-white dark:bg-black rounded-2xl border border-slate-200 dark:border-slate-800 p-8 shadow-inner overflow-hidden relative flex flex-col items-center justify-center">
                
                {isGenerating ? (
                    <div className="flex flex-col items-center animate-pulse">
                        <span className="text-4xl mb-4 animate-spin"></span>
                        <div className="text-gray-400 font-mono text-sm">Generating Component Tree...</div>
                    </div>
                ) : (
                    <div className="w-full max-w-md animate-in zoom-in fade-in duration-500">
                        {uiState.type === 'empty' && (
                            <div className="text-center text-gray-400">
                                <span className="text-6xl mx-auto mb-4 block opacity-50"></span>
                                <p>Ask for a UI and I will build it.</p>
                            </div>
                        )}

                        {uiState.type === 'chart' && (
                            <div className="bg-white dark:bg-slate-900 p-6 rounded-xl border border-slate-200 dark:border-slate-800 shadow-lg">
                                <h4 className="font-bold mb-4 flex items-center gap-2"><span>📊</span> Q1 Sales Performance</h4>
                                <div className="h-32 flex items-end gap-2">
                                    <div className="flex-1 bg-blue-100 dark:bg-blue-900/30 h-[60%] rounded-t"></div>
                                    <div className="flex-1 bg-blue-500 h-[80%] rounded-t shadow-lg shadow-blue-500/50"></div>
                                    <div className="flex-1 bg-blue-100 dark:bg-blue-900/30 h-[40%] rounded-t"></div>
                                </div>
                                <div className="mt-4 text-xs text-gray-500 text-center">Generated via `&lt;SalesChart /&gt;` tool</div>
                            </div>
                        )}

                        {uiState.type === 'calendar' && (
                            <div className="bg-white dark:bg-slate-900 p-6 rounded-xl border border-slate-200 dark:border-slate-800 shadow-lg">
                                <h4 className="font-bold mb-4 flex items-center gap-2"><span>📅</span> Schedule Meeting</h4>
                                <div className="grid grid-cols-7 gap-2 mb-4">
                                    {[...Array(7)].map((_, i) => <div key={i} className="text-center text-xs text-gray-400">D</div>)}
                                    {[...Array(7)].map((_, i) => (
                                        <div key={i} className={`aspect-square rounded flex items-center justify-center text-xs ${i === 3 ? 'bg-purple-500 text-white' : 'bg-gray-100 dark:bg-slate-800'}`}>
                                            {i+10}
                                        </div>
                                    ))}
                                </div>
                                <button className="w-full py-2 bg-purple-100 text-purple-600 rounded-lg font-bold text-sm">Confirm Slot</button>
                                <div className="mt-4 text-xs text-gray-500 text-center">Generated via `&lt;SchedulePicker /&gt;` tool</div>
                            </div>
                        )}

                        {uiState.type === 'payment' && (
                            <div className="bg-white dark:bg-slate-900 p-6 rounded-xl border border-slate-200 dark:border-slate-800 shadow-lg">
                                <h4 className="font-bold mb-4 flex items-center gap-2"><span>💳</span> Update Method</h4>
                                <div className="space-y-3">
                                    <input disabled placeholder="**** **** **** 4242" className="w-full p-2 bg-gray-50 dark:bg-slate-800 rounded border border-gray-200 dark:border-slate-700 text-sm" />
                                    <div className="flex gap-2">
                                        <input disabled placeholder="MM/YY" className="w-1/2 p-2 bg-gray-50 dark:bg-slate-800 rounded border border-gray-200 dark:border-slate-700 text-sm" />
                                        <input disabled placeholder="CVC" className="w-1/2 p-2 bg-gray-50 dark:bg-slate-800 rounded border border-gray-200 dark:border-slate-700 text-sm" />
                                    </div>
                                </div>
                                <button className="w-full mt-4 py-2 bg-green-500 text-white rounded-lg font-bold text-sm shadow-lg shadow-green-500/30">Save Card</button>
                                <div className="mt-4 text-xs text-gray-500 text-center">Generated via `&lt;StripeForm /&gt;` tool</div>
                            </div>
                        )}
                        
                        {uiState.type === 'text' && (
                             <div className="bg-gray-100 dark:bg-slate-800 p-4 rounded-xl text-sm">
                                {uiState.data}
                             </div>
                        )}
                    </div>
                )}
            </div>

            {/* Input Area */}
            <div className="mt-6 flex flex-col gap-4">
                <div className="flex gap-2 overflow-x-auto pb-2">
                    {prompts.map((p, i) => (
                        <button 
                            key={i}
                            onClick={() => handleSubmit(p)}
                            disabled={isGenerating}
                            className="whitespace-nowrap px-4 py-1.5 rounded-full bg-slate-100 dark:bg-slate-900 text-xs font-medium hover:bg-slate-200 dark:hover:bg-slate-800 transition"
                        >
                            {p}
                        </button>
                    ))}
                </div>
                
                <div className="relative">
                    <input 
                        value={prompt}
                        onChange={(e) => setPrompt(e.target.value)}
                        placeholder="Describe the UI you need..."
                        className="w-full p-4 pr-12 rounded-xl bg-white dark:bg-slate-900 border border-slate-200 dark:border-slate-800 focus:ring-2 focus:ring-pink-500 outline-none transition"
                        onKeyDown={(e) => e.key === 'Enter' && handleSubmit(prompt)}
                    />
                    <button 
                        onClick={() => handleSubmit(prompt)}
                        disabled={!prompt.trim() || isGenerating}
                        className="absolute right-2 top-1/2 -translate-y-1/2 w-8 h-8 flex items-center justify-center bg-pink-500 text-white rounded-lg hover:bg-pink-600 disabled:opacity-50 transition"
                    >
                        <span>⬆️</span>
                    </button>
                </div>
            </div>

        </div>
    );
}