shadcn.io is an independent project built on shadcn/ui
A browser inside the app: back, forward and reload, an address field, the page in a sandboxed frame, and a console under it that opens onto the page's logs.
Use it to show what generated code renders, a dev server's page or the HTML a model just wrote, beside the chat, where the reader can click through it and read its errors. For a still screenshot in a browser window on a landing page, use Safari: a mockup with no history and nothing to type into. In Vue, the toolbar is Buttons with Tooltips and an Input, the console a Collapsible, all from Ark UI.
Installation
Anatomy

WebPreview
├── WebPreviewNavigation
│ ├── WebPreviewNavigationButton back, forward, reload, your own
│ └── WebPreviewUrl
├── WebPreviewBody the iframe, loading over it
└── WebPreviewConsole the logs, colored by levelUsage
<script setup lang="ts">
import {
ArrowLeftIcon, ArrowRightIcon, RefreshCcwIcon,
} from "lucide-vue-next";
import {
WebPreview, WebPreviewBody, WebPreviewConsole, WebPreviewNavigation,
WebPreviewNavigationButton, WebPreviewUrl,
} from "@/components/ui/web-preview";
</script>
<template>
<WebPreview class="h-96" defaultUrl="http://localhost:3000/">
<WebPreviewNavigation>
<WebPreviewNavigationButton action="back" tooltip="Go back">
<ArrowLeftIcon aria-hidden="true" />
</WebPreviewNavigationButton>
<WebPreviewNavigationButton action="forward" tooltip="Go forward">
<ArrowRightIcon aria-hidden="true" />
</WebPreviewNavigationButton>
<WebPreviewNavigationButton action="reload" tooltip="Reload">
<RefreshCcwIcon aria-hidden="true" />
</WebPreviewNavigationButton>
<WebPreviewUrl />
</WebPreviewNavigation>
<WebPreviewBody />
<WebPreviewConsole />
</WebPreview>
</template>
action gives a button its job in the preview's history: back and forward disable themselves at either end, and reload loads the page again. A button without one does what its handler says, like the first demo's open-in-a-new-tab and full-screen buttons.
Demos
Console
The page's own console.log, console.warn and uncaught errors land in WebPreviewConsole: a script in the page posts each one to its parent, and the demo adds it to logs. Warnings take the warning color and errors the destructive one, each with an icon, so the level doesn't rest on color alone. The console starts open here with defaultOpen; its trigger counts the lines.
Controlled
url and onUrlChange put the address in your state, so the list of pages beside the preview, the address field, the page's own links and Back all move the same address. A link in srcDoc HTML would open your own site's path in the frame, so the page posts the address it wants instead. Every address set from outside is a visit too: Back returns from it.
Loading
loading covers the frame while it's set: here a spinner while the model writes the next version of the page. It shows whatever you pass, for as long as you pass it, so set it from your own state, such as a tool call that hasn't finished. Press the sparkles to build another version.
AI SDK
A model builds a page through a tool: createPage here, whose output is the HTML. Give the HTML to WebPreviewBody as srcDoc once the call is done, and cover the frame while it runs.
import { Chat } from "@ai-sdk/vue";
import { computed } from "vue";
const chat = new Chat({});
const page = computed(() =>
chat.messages
.flatMap((message) => message.parts)
.filter((part) => part.type === "tool-createPage")
.at(-1)
);When the tool starts a dev server in a sandbox instead, it returns the server's address: pass that as url, or as defaultUrl the first time, and leave srcDoc out.
Migrating from AI SDK Elements
AI SDK Elements is for React. In Vue, start from the demos above: the parts and their names are the same.
FAQ
How do I install the Web Preview in Vue?
Run the command in the Installation section: it adds web-preview to your components/ui folder with the Button, Tooltip, Input and Collapsible it's built on.
The command uses the shadcn-vue CLI.
When should I use Web Preview instead of Safari?
When the page is live and the reader should use it: navigate, reload, read its console. Safari is a picture of a browser around a screenshot, a video or a page, for a landing page or a changelog, with nothing to type into.
How do I preview HTML a model wrote instead of a URL?
Pass it to WebPreviewBody as srcDoc. It runs in a sandbox with an origin of its own, so its scripts can't reach your page's cookies or storage. Its links resolve against your site, so have the page post the address it wants and set url, as the Controlled demo does.
Why does the Web Preview show a blank page for some sites?
Those sites refuse to be framed, with an X-Frame-Options header or a frame-ancestors rule, and the browser shows nothing; no component can get around it. Preview your own dev server or the generated HTML instead, and keep an open-in-a-new-tab button for the rest.
Do back and forward work in the Web Preview?
Yes, on the addresses the preview opened: what was typed and entered, and what you set through url. Give a button action="back" or action="forward" and it moves through them, disabled at either end. Navigation inside the frame isn't recorded: a page on another origin can't be read from yours.
How do I show the page's console logs?
Pass them to WebPreviewConsole as logs, each a level (log, warn or error), a message and a timestamp. For your own HTML, add a script that posts each console call to the parent and append what arrives, as the Console demo does; for a sandboxed dev server, stream its logs from your server.
API Reference
WebPreview
url?string
The address, controlled: set it with onUrlChange. Two-way with v-model:url.
defaultUrl?string
The address it opens at, when url isn't set.
- Default
""
class?string
Merged with the part's classes.
@url-change(url: string) → void
Called with the new address when the reader navigates.
- Parameters
url– string- Returns
void
WebPreviewNavigation
<div> data-slot="web-preview-navigation"
class?string
Merged with the part's classes.
WebPreviewNavigationButton
data-slot="web-preview-navigation-button"
action?"back" | "forward" | "reload"
What it does in the preview's history: back, forward (each disabled
at its end) or reload. Without it, it does what onClick says.
- Type
"back" | "forward" | "reload"
clickEffect?boolean
Apply a click effect to the button.
- Default
true
isLoading?boolean
Show a loading indicator.
- Default
false
tooltip?string
Shown on hover and focus, and its accessible name.
asChild?boolean
Use the provided child element as the default rendered element, combining their props and behavior.
class?string
Merged with the part's classes.
WebPreviewUrl
data-slot="web-preview-url"
asChild?boolean
Use the provided child element as the default rendered element, combining their props and behavior.
class?string
Merged with the part's classes.
WebPreviewBody
<div> data-slot="web-preview-body"
loading?VNode
Shown over the page while it's set: a spinner while the page is built.
src?string
The page's address, in place of the preview's.
srcDoc?string
The page's HTML, in place of an address: what a model just wrote.
class?string
Merged with the part's classes.
WebPreviewConsole
data-slot="web-preview-console"
open?boolean
The controlled open state of the collapsible. Two-way with v-model:open.
defaultOpen?boolean
The initial open state of the collapsible when rendered. Use when you don't need to control the open state of the collapsible.
collapsedHeight?number | string
The height of the content when collapsed.
- Type
number | string
collapsedWidth?number | string
The width of the content when collapsed.
- Type
number | string
dir?"ltr" | "rtl"
The document's text/writing direction.
- Type
"ltr" | "rtl"- Default
"ltr"
disabled?boolean
Whether the collapsible is disabled.
hideMode?"display-none" | "activity"
How to hide content when mounted but not present.
- display-none: HTML hidden attribute. Effects stay alive.
- activity: React 19 <Activity mode="hidden">. Effects pause. Requires React 19+.
- Type
"display-none" | "activity"- Default
"display-none"
id?string
The unique identifier of the machine.
ids?object
The ids of the elements in the collapsible. Useful for composition.
- Type
{ root?: string; content?: string; trigger?: string }
lazyMount?boolean
Whether to enable lazy mounting.
- Default
false
logs?WebPreviewLog[]
The lines to show, oldest first, each colored by its level.
- Default
[]
onExitComplete?VoidFunction
The callback invoked when the exit animation completes.
unmountOnExit?boolean
Whether to unmount on exit.
- Default
false
asChild?boolean
Use the provided child element as the default rendered element, combining their props and behavior.
class?string
Merged with the part's classes.
@open-change(details: OpenChangeDetails) → void
The callback invoked when the open state changes.
- Type
(details: OpenChangeDetails) → void- Parameters
details– { open: boolean }- Returns
void
data-level?string
Mirrors level.













