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):

Terminal
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:

Terminal
npx ai-elements@latest

Now, install the v0 sdk:

pnpm
npm
yarn
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.

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

components/ai-elements/web-preview.tsx
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.

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