v0 clone
An example of how to use the AI Elements to build a v0 clone.
Tutorial
Let's walk through how to build a v0 clone using AI Elements and the v0 Platform API.
Setup
First, set up a new Next.js repo and cd into it by running the following command (make sure you choose to use Tailwind the project setup):
npx create-next-app@latest v0-clone && cd v0-clone
Run the following command to install AI Elements. This will also set up shadcn/ui if you haven't already configured it:
npx ai-elements@latest
Now, install the v0 sdk:
pnpm add v0-sdk
In order to use the providers, let's configure a v0 API key. Create a .env.local
in your root directory and navigate to your v0 account settings to create a token, then paste it in your .env.local
as V0_API_KEY
.
We're now ready to start building our app!
Client
In your app/page.tsx
, replace the code with the file below.
Here, we use Conversation
to wrap the conversation code, and the WebPreview
component to render the URL returned from the v0 API.
'use client';
import { useState } from 'react';
import { Input, PromptInputSubmit, PromptInputTextarea,} from '@/components/ai-elements/prompt-input';import { Message, MessageContent } from '@/components/ai-elements/message';import { Conversation, ConversationContent,} from '@/components/ai-elements/conversation';import { WebPreview, WebPreviewNavigation, WebPreviewUrl, WebPreviewBody,} from '@/components/ai-elements/web-preview';import { Loader } from '@/components/ai-elements/loader';import { Suggestions, Suggestion } from '@/components/ai-elements/suggestion';
interface Chat { id: string; demo: string;}
export default function Home() { const [message, setMessage] = useState(''); const [currentChat, setCurrentChat] = useState<Chat | null>(null); const [isLoading, setIsLoading] = useState(false); const [chatHistory, setChatHistory] = useState< Array<{ type: 'user' | 'assistant'; content: string; }> >([]);
const handleSendMessage = async (e: React.FormEvent<HTMLFormElement>) => { e.preventDefault(); if (!message.trim() || isLoading) return;
const userMessage = message.trim(); setMessage(''); setIsLoading(true);
setChatHistory((prev) => [...prev, { type: 'user', content: userMessage }]);
try { const response = await fetch('/api/chat', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ message: userMessage, chatId: currentChat?.id, }), });
if (!response.ok) { throw new Error('Failed to create chat'); }
const chat: Chat = await response.json(); setCurrentChat(chat);
setChatHistory((prev) => [ ...prev, { type: 'assistant', content: 'Generated new app preview. Check the preview panel!', }, ]); } catch (error) { console.error('Error:', error); setChatHistory((prev) => [ ...prev, { type: 'assistant', content: 'Sorry, there was an error creating your app. Please try again.', }, ]); } finally { setIsLoading(false); } };
return ( <div className="h-screen flex"> {/* Chat Panel */} <div className="w-1/2 flex flex-col border-r"> {/* Header */} <div className="border-b p-3 h-14 flex items-center justify-between"> <h1 className="text-lg font-semibold">v0 Clone</h1> </div>
<div className="flex-1 overflow-y-auto p-4 space-y-4"> {chatHistory.length === 0 ? ( <div className="text-center font-semibold mt-8"> <p className="text-3xl mt-4">What can we build together?</p> </div> ) : ( <> <Conversation> <ConversationContent> {chatHistory.map((msg, index) => ( <Message from={msg.type} key={index}> <MessageContent>{msg.content}</MessageContent> </Message> ))} </ConversationContent> </Conversation> {isLoading && ( <Message from="assistant"> <MessageContent> <p className="flex items-center gap-2"> <Loader /> Creating your app... </p> </MessageContent> </Message> )} </> )} </div>
{/* Input */} <div className="border-t p-4"> {!currentChat && ( <Suggestions> <Suggestion onClick={() => setMessage('Create a responsive navbar with Tailwind CSS') } suggestion="Create a responsive navbar with Tailwind CSS" /> <Suggestion onClick={() => setMessage('Build a todo app with React')} suggestion="Build a todo app with React" /> <Suggestion onClick={() => setMessage('Make a landing page for a coffee shop') } suggestion="Make a landing page for a coffee shop" /> </Suggestions> )} <div className="flex gap-2"> <Input onSubmit={handleSendMessage} className="mt-4 w-full max-w-2xl mx-auto relative" > <PromptInputTextarea onChange={(e) => setMessage(e.target.value)} value={message} className="pr-12 min-h-[60px]" /> <PromptInputSubmit className="absolute bottom-1 right-1" disabled={!message} status={isLoading ? 'streaming' : 'ready'} /> </Input> </div> </div> </div>
{/* Preview Panel */} <div className="w-1/2 flex flex-col"> <WebPreview> <WebPreviewNavigation> <WebPreviewUrl placeholder="Your app here..." value={currentChat?.demo} /> </WebPreviewNavigation> <WebPreviewBody src={currentChat?.demo} /> </WebPreview> </div> </div> );}
In this case, we'll also edit the base component components/ai-elements/web-preview.tsx
in order to best match with our theme.
return ( <WebPreviewContext.Provider value={contextValue}> <div className={cn( 'flex size-full flex-col bg-card', // remove rounded-lg border 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 h-14', className)} // add h-14 {...props} > {children} </div>);
Server
Create a new route handler app/api/chat/route.ts
and paste in the following code. We use the v0 SDK to manage chats.
import { NextRequest, NextResponse } from 'next/server';import { v0 } from 'v0-sdk';
export async function POST(request: NextRequest) { try { const { message, chatId } = await request.json();
if (!message) { return NextResponse.json( { error: 'Message is required' }, { status: 400 }, ); }
let chat;
if (chatId) { // continue existing chat chat = await v0.chats.sendMessage({ chatId: chatId, message, }); } else { // create new chat chat = await v0.chats.create({ message, }); }
return NextResponse.json({ id: chat.id, demo: chat.demo, }); } catch (error) { console.error('V0 API Error:', error); return NextResponse.json( { error: 'Failed to process request' }, { status: 500 }, ); }}
You now have a working v0 clone you can build off of! Feel free to explore the v0 Platform API and components like Reasoning
and Task
to extend your app, or view the other examples.