Vibe coding
Web Preview
A composable component for previewing the result of a generated UI, with support for live examples and code display.
The WebPreview component provides a flexible way to showcase the result of a generated UI component, along with its source code. It is designed for documentation and demo purposes, allowing users to interact with live examples and view the underlying implementation.
"use client";import { WebPreview, WebPreviewBody, WebPreviewConsole, WebPreviewNavigation, WebPreviewNavigationButton, WebPreviewUrl,} from "@/components/ai-elements/elements/web-preview";import { ArrowLeftIcon, ArrowRightIcon, ExternalLinkIcon, Maximize2Icon, MousePointerClickIcon, RefreshCcwIcon,} from "lucide-react";import { useState } from "react";const exampleLogs = [ { level: "log" as const, message: "Page loaded successfully", timestamp: new Date(Date.now() - 10_000), }, { level: "warn" as const, message: "Deprecated API usage detected", timestamp: new Date(Date.now() - 5000), }, { level: "error" as const, message: "Failed to load resource", timestamp: new Date(), },];const code = [ { language: "jsx", filename: "MyComponent.jsx", code: `function MyComponent(props) { return ( <div> <h1>Hello, {props.name}!</h1> <p>This is an example React component.</p> </div> );}`, },];const Example = () => { const [fullscreen, setFullscreen] = useState(false); return ( <WebPreview defaultUrl="/" onUrlChange={(url) => console.log("URL changed to:", url)} style={{ height: "400px" }} > <WebPreviewNavigation> <WebPreviewNavigationButton onClick={() => console.log("Go back")} tooltip="Go back" > <ArrowLeftIcon className="size-4" /> </WebPreviewNavigationButton> <WebPreviewNavigationButton onClick={() => console.log("Go forward")} tooltip="Go forward" > <ArrowRightIcon className="size-4" /> </WebPreviewNavigationButton> <WebPreviewNavigationButton onClick={() => console.log("Reload")} tooltip="Reload" > <RefreshCcwIcon className="size-4" /> </WebPreviewNavigationButton> <WebPreviewUrl /> <WebPreviewNavigationButton onClick={() => console.log("Select")} tooltip="Select" > <MousePointerClickIcon className="size-4" /> </WebPreviewNavigationButton> <WebPreviewNavigationButton onClick={() => console.log("Open in new tab")} tooltip="Open in new tab" > <ExternalLinkIcon className="size-4" /> </WebPreviewNavigationButton> <WebPreviewNavigationButton onClick={() => setFullscreen(!fullscreen)} tooltip="Maximize" > <Maximize2Icon className="size-4" /> </WebPreviewNavigationButton> </WebPreviewNavigation> <WebPreviewBody src="https://preview-v0me-kzml7zc6fkcvbyhzrf47.vusercontent.net/" /> <WebPreviewConsole logs={exampleLogs} /> </WebPreview> );};export default Example;Installation
npx ai-elements@latest add web-previewnpx shadcn@latest add @ai-elements/web-preview"use client";import { Button } from "@repo/shadcn-ui/components/ui/button";import { Collapsible, CollapsibleContent, CollapsibleTrigger,} from "@repo/shadcn-ui/components/ui/collapsible";import { Input } from "@repo/shadcn-ui/components/ui/input";import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger,} from "@repo/shadcn-ui/components/ui/tooltip";import { cn } from "@repo/shadcn-ui/lib/utils";import { ChevronDownIcon } from "lucide-react";import type { ComponentProps, ReactNode } from "react";import { createContext, useContext, useEffect, useState } from "react";export type WebPreviewContextValue = { url: string; setUrl: (url: string) => void; consoleOpen: boolean; setConsoleOpen: (open: boolean) => void;};const WebPreviewContext = createContext<WebPreviewContextValue | null>(null);const useWebPreview = () => { const context = useContext(WebPreviewContext); if (!context) { throw new Error("WebPreview components must be used within a WebPreview"); } return context;};export type WebPreviewProps = ComponentProps<"div"> & { defaultUrl?: string; onUrlChange?: (url: string) => void;};export const WebPreview = ({ className, children, defaultUrl = "", onUrlChange, ...props}: WebPreviewProps) => { const [url, setUrl] = useState(defaultUrl); const [consoleOpen, setConsoleOpen] = useState(false); const handleUrlChange = (newUrl: string) => { setUrl(newUrl); onUrlChange?.(newUrl); }; const contextValue: WebPreviewContextValue = { url, setUrl: handleUrlChange, consoleOpen, setConsoleOpen, }; return ( <WebPreviewContext.Provider value={contextValue}> <div className={cn( "flex size-full flex-col rounded-lg border bg-card", className )} {...props} > {children} </div> </WebPreviewContext.Provider> );};export type WebPreviewNavigationProps = ComponentProps<"div">;export const WebPreviewNavigation = ({ className, children, ...props}: WebPreviewNavigationProps) => ( <div className={cn("flex items-center gap-1 border-b p-2", className)} {...props} > {children} </div>);export type WebPreviewNavigationButtonProps = ComponentProps<typeof Button> & { tooltip?: string;};export const WebPreviewNavigationButton = ({ onClick, disabled, tooltip, children, ...props}: WebPreviewNavigationButtonProps) => ( <TooltipProvider> <Tooltip> <TooltipTrigger asChild> <Button className="h-8 w-8 p-0 hover:text-foreground" disabled={disabled} onClick={onClick} size="sm" variant="ghost" {...props} > {children} </Button> </TooltipTrigger> <TooltipContent> <p>{tooltip}</p> </TooltipContent> </Tooltip> </TooltipProvider>);export type WebPreviewUrlProps = ComponentProps<typeof Input>;export const WebPreviewUrl = ({ value, onChange, onKeyDown, ...props}: WebPreviewUrlProps) => { const { url, setUrl } = useWebPreview(); const [inputValue, setInputValue] = useState(url); // Sync input value with context URL when it changes externally useEffect(() => { setInputValue(url); }, [url]); const handleChange = (event: React.ChangeEvent<HTMLInputElement>) => { setInputValue(event.target.value); onChange?.(event); }; const handleKeyDown = (event: React.KeyboardEvent<HTMLInputElement>) => { if (event.key === "Enter") { const target = event.target as HTMLInputElement; setUrl(target.value); } onKeyDown?.(event); }; return ( <Input className="h-8 flex-1 text-sm" onChange={onChange ?? handleChange} onKeyDown={handleKeyDown} placeholder="Enter URL..." value={value ?? inputValue} {...props} /> );};export type WebPreviewBodyProps = ComponentProps<"iframe"> & { loading?: ReactNode;};export const WebPreviewBody = ({ className, loading, src, ...props}: WebPreviewBodyProps) => { const { url } = useWebPreview(); return ( <div className="flex-1"> <iframe className={cn("size-full", className)} sandbox="allow-scripts allow-same-origin allow-forms allow-popups allow-presentation" src={(src ?? url) || undefined} title="Preview" {...props} /> {loading} </div> );};export type WebPreviewConsoleProps = ComponentProps<"div"> & { logs?: Array<{ level: "log" | "warn" | "error"; message: string; timestamp: Date; }>;};export const WebPreviewConsole = ({ className, logs = [], children, ...props}: WebPreviewConsoleProps) => { const { consoleOpen, setConsoleOpen } = useWebPreview(); return ( <Collapsible className={cn("border-t bg-muted/50 font-mono text-sm", className)} onOpenChange={setConsoleOpen} open={consoleOpen} {...props} > <CollapsibleTrigger asChild> <Button className="flex w-full items-center justify-between p-4 text-left font-medium hover:bg-muted/50" variant="ghost" > Console <ChevronDownIcon className={cn( "h-4 w-4 transition-transform duration-200", consoleOpen && "rotate-180" )} /> </Button> </CollapsibleTrigger> <CollapsibleContent className={cn( "px-4 pb-4", "data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 outline-none data-[state=closed]:animate-out data-[state=open]:animate-in" )} > <div className="max-h-48 space-y-1 overflow-y-auto"> {logs.length === 0 ? ( <p className="text-muted-foreground">No console output</p> ) : ( logs.map((log, index) => ( <div className={cn( "text-xs", log.level === "error" && "text-destructive", log.level === "warn" && "text-yellow-600", log.level === "log" && "text-foreground" )} key={`${log.timestamp.getTime()}-${index}`} > <span className="text-muted-foreground"> {log.timestamp.toLocaleTimeString()} </span>{" "} {log.message} </div> )) )} {children} </div> </CollapsibleContent> </Collapsible> );};Usage
import {
WebPreview,
WebPreviewNavigation,
WebPreviewUrl,
WebPreviewBody,
} from '@/components/ai-elements/web-preview';<WebPreview defaultUrl="https://ai-sdk.dev" style={{ height: '400px' }}>
<WebPreviewNavigation>
<WebPreviewUrl src="https://ai-sdk.dev" />
</WebPreviewNavigation>
<WebPreviewBody src="https://ai-sdk.dev" />
</WebPreview>Usage with AI SDK
Build a simple v0 clone using the v0 Platform API.
Install the v0-sdk package:
npm i v0-sdkAdd the following component to your frontend:
'use client';
import {
WebPreview,
WebPreviewBody,
WebPreviewNavigation,
WebPreviewUrl,
} from '@/components/ai-elements/web-preview';
import { useState } from 'react';
import {
Input,
PromptInputTextarea,
PromptInputSubmit,
} from '@/components/ai-elements/prompt-input';
import { Loader } from '../ai-elements/loader';
const WebPreviewDemo = () => {
const [previewUrl, setPreviewUrl] = useState('');
const [prompt, setPrompt] = useState('');
const [isGenerating, setIsGenerating] = useState(false);
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
if (!prompt.trim()) return;
setPrompt('');
setIsGenerating(true);
try {
const response = await fetch('/api/v0', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ prompt }),
});
const data = await response.json();
setPreviewUrl(data.demo || '/');
console.log('Generation finished:', data);
} catch (error) {
console.error('Generation failed:', error);
} finally {
setIsGenerating(false);
}
};
return (
<div className="max-w-4xl mx-auto p-6 relative size-full rounded-lg border h-[600px]">
<div className="flex flex-col h-full">
<div className="flex-1 mb-4">
{isGenerating ? (
<div className="flex flex-col items-center justify-center h-full">
<Loader />
<p className="mt-4 text-muted-foreground">
Generating app, this may take a few seconds...
</p>
</div>
) : previewUrl ? (
<WebPreview defaultUrl={previewUrl}>
<WebPreviewNavigation>
<WebPreviewUrl />
</WebPreviewNavigation>
<WebPreviewBody src={previewUrl} />
</WebPreview>
) : (
<div className="flex items-center justify-center h-full text-muted-foreground">
Your generated app will appear here
</div>
)}
</div>
<Input
onSubmit={handleSubmit}
className="w-full max-w-2xl mx-auto relative"
>
<PromptInputTextarea
value={prompt}
placeholder="Describe the app you want to build..."
onChange={(e) => setPrompt(e.currentTarget.value)}
className="pr-12 min-h-[60px]"
/>
<PromptInputSubmit
status={isGenerating ? 'streaming' : 'ready'}
disabled={!prompt.trim()}
className="absolute bottom-1 right-1"
/>
</Input>
</div>
</div>
);
};
export default WebPreviewDemo;Add the following route to your backend:
import { v0 } from 'v0-sdk';
export async function POST(req: Request) {
const { prompt }: { prompt: string } = await req.json();
const result = await v0.chats.create({
system: 'You are an expert coder',
message: prompt,
modelConfiguration: {
modelId: 'v0-1.5-sm',
imageGenerations: false,
thinking: false,
},
});
return Response.json({
demo: result.demo,
webUrl: result.webUrl,
});
}Features
- Live preview of UI components
- Composable architecture with dedicated sub-components
- Responsive design modes (Desktop, Tablet, Mobile)
- Navigation controls with back/forward functionality
- URL input and example selector
- Full screen mode support
- Console logging with timestamps
- Context-based state management
- Consistent styling with the design system
- Easy integration into documentation pages
Props
<WebPreview />
Prop
Type
<WebPreviewNavigation />
Prop
Type
<WebPreviewNavigationButton />
Prop
Type
<WebPreviewUrl />
Prop
Type
<WebPreviewBody />
Prop
Type
<WebPreviewConsole />
Prop
Type