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-preview
npx 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-sdk

Add the following component to your frontend:

app/page.tsx
'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:

app/api/v0/route.ts
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