---
title: "Live Preview — Overview"
aliases: [live-preview, payload-live-preview, visual-editing]
tags: [payloadcms, tech-patterns, live-preview]
sources:
  - raw/live-preview__overview.md
  - https://payloadcms.com/docs/live-preview/overview
  - https://payloadcms.com/docs/live-preview/server
  - https://payloadcms.com/docs/live-preview/client
  - https://payloadcms.com/docs/live-preview/frontend
created: 2026-05-15
updated: 2026-05-15
---

# PayloadCMS — Live Preview

## Overview

- Renders your frontend inside an iframe directly in the Admin Panel
- Changes propagate in real-time via `window.postMessage` — no save required
- Two modes: **server-side** (React Server Components / Next.js App Router) and **client-side** (Pages Router, Vue, SPA)
- Server-side is recommended when available: simpler setup, better performance
- Client-side fires on every form-state change; server-side fires on save/autosave/publish

## Setup

### Payload config (global or per-collection)

```ts
import { buildConfig } from 'payload'

export default buildConfig({
  admin: {
    livePreview: {
      url: 'http://localhost:3000',
      collections: ['pages'],
      globals: ['header'],
    },
  },
})
```

- Can also be set on individual [[wiki/payloadcms/configuration|Collection]] or Global admin configs; those settings are merged as overrides
- Once configured, a "Live Preview" toggle appears at the top of each enabled document

## Config Options

| Option        | Type                        | Description                                              |
| ------------- | --------------------------- | -------------------------------------------------------- |
| `url`         | `string \| function`        | iframe `src`; can be dynamic (see below)                 |
| `breakpoints` | `Breakpoint[]`              | Device-size presets shown in the toolbar                 |
| `collections` | `string[]`                  | Collection slugs to enable Live Preview on               |
| `globals`     | `string[]`                  | Global slugs to enable Live Preview on                   |

### Dynamic URL function

```ts
url: ({ data, collectionConfig, locale, req }) =>
  `${data.tenant.url}/${data.slug}${locale ? `?locale=${locale.code}` : ''}`
```

**URL function args:**

| Arg | Description |
|-----|-------------|
| `data` | Document data including unsaved changes |
| `locale` | Current locale being edited (if localization enabled) |
| `collectionConfig` | Collection admin config of the document |
| `globalConfig` | Global admin config of the document |
| `req` | Payload Request object (use for fully-qualified URLs: `${req.protocol}//${req.host}/${data.slug}`) |

- Return `undefined` or `null` to conditionally hide Live Preview (acts as access control)
- Return a relative path — Payload auto-injects protocol/host from the browser window
- Useful for unknown preview URLs at build time (e.g. Vercel preview deployments)

#### Conditional Rendering

Return `null`/`undefined` to hide the Live Preview button for certain users or documents:

```ts
url: ({ req }) => (req.user?.role === 'admin' ? '/hello-world' : null)
```

### Breakpoints

```ts
breakpoints: [
  { label: 'Mobile', name: 'mobile', width: 375, height: 667 },
  { label: 'Tablet', name: 'tablet', width: 768, height: 1024 },
]
```

**Breakpoint options** (* = required):

| Option | Description |
|--------|-------------|
| `label` * | Label shown in the toolbar dropdown |
| `name` * | Internal identifier |
| `width` * | iframe width in px |
| `height` * | iframe height in px |

- **"Responsive"** (100% width/height) is always available and is the default — no config needed
- Toolbar has manual width/height inputs that temporarily switch to "Custom" mode
- **"Open in new window"** button closes the iframe and opens a resizable browser window; closing it re-opens the iframe

## Code Examples

### Server-side (Next.js App Router)

Install:
```bash
npm install @payloadcms/live-preview-react
```

`page.tsx` (Server Component):
```tsx
import { RefreshRouteOnSave } from './RefreshRouteOnSave'
import { getPayload } from 'payload'
import config from '../payload.config'

export default async function Page() {
  const payload = await getPayload({ config })
  const page = await payload.findByID({ collection: 'pages', id: '123', draft: true })
  return (
    <>
      <RefreshRouteOnSave />
      <h1>{page.title}</h1>
    </>
  )
}
```

`RefreshRouteOnSave.tsx` (Client Component):
```tsx
'use client'
import { RefreshRouteOnSave as PayloadLivePreview } from '@payloadcms/live-preview-react'
import { useRouter } from 'next/navigation'

export const RefreshRouteOnSave = () => {
  const router = useRouter()
  return (
    <PayloadLivePreview
      refresh={() => router.refresh()}
      serverURL={process.env.NEXT_PUBLIC_PAYLOAD_URL}
    />
  )
}
```

### Client-side React (Pages Router / SPA)

```tsx
'use client'
import { useLivePreview } from '@payloadcms/live-preview-react'
import type { Page as PageType } from '@/payload-types'

export const PageClient: React.FC<{ page: PageType }> = ({ page: initialPage }) => {
  const { data, isLoading } = useLivePreview<PageType>({
    initialData: initialPage,
    serverURL: process.env.NEXT_PUBLIC_PAYLOAD_URL,
    depth: 2, // must match your initial fetch depth
  })
  return <h1>{data.title}</h1>
}
```

### Client-side Vue 3 / Nuxt 3

```bash
npm install @payloadcms/live-preview-vue
```

```vue
<script setup lang="ts">
import { useLivePreview } from '@payloadcms/live-preview-vue'
const props = defineProps<{ initialData: PageData }>()
const { data } = useLivePreview<PageData>({
  initialData: props.initialData,
  serverURL: '<PAYLOAD_SERVER_URL>',
  depth: 2,
})
</script>
<template><h1>{{ data.title }}</h1></template>
```

### Custom hook (framework-agnostic)

```bash
npm install @payloadcms/live-preview
```

```ts
import { subscribe, unsubscribe, ready } from '@payloadcms/live-preview'
// 1. subscribe() — listens to postMessage, merges data, populates relationships, calls callback
// 2. ready()     — signals Admin Panel the frontend is ready
// 3. unsubscribe() — clean up on unmount
```

For server-side custom: use `ready` + `isDocumentEvent` from the same package to trigger `router.refresh()`.

## Gotchas

- **Depth mismatch** — `depth` in `useLivePreview` must exactly match the depth used in the initial page fetch; otherwise relationships disappear during editing
- **CSP iframe block** — add `frame-ancestors: "self" localhost:* https://your-cms.com` to your frontend's Content Security Policy
- **CORS/CSRF** — if frontend and Payload run on different domains/ports, configure both `cors` and `csrf` in `payload.config.ts`
- **Server-side feels slower** — it refreshes on save, not on keystroke. Fix: enable [[wiki/payloadcms/versions|Autosave]] with a short interval (e.g. `375ms`)
- **RSC + client hook** — do not use `useLivePreview` in Server Components; wrap in a `'use client'` component
- **Conditional rendering** — returning `null` from `url` function hides the Live Preview button; use it for role-based access

## Key Takeaways

- Live Preview renders your frontend in an **iframe** inside the Admin Panel; no save required for client-side mode
- Uses `window.postMessage` for real-time sync between Admin Panel and frontend
- `url` can be a **dynamic function** — use it for multi-tenant, localization, unknown preview URLs, or conditional access
- Return `null`/`undefined` from `url` to hide Live Preview for specific users or documents (role-based access control)
- **Depth mismatch** in `useLivePreview` vs initial fetch is the most common bug — always match them
- Server-side mode (Next.js App Router) = simpler, but fires on save; client-side = fires on every keystroke
- "Responsive" breakpoint is always available; "Open in new window" for free resize

## Related

- [[wiki/payloadcms/live-preview-client|Live Preview — Client-Side]] — `useLivePreview` hook, Vue, framework-agnostic API
- [[wiki/payloadcms/live-preview-frontend|Live Preview — Frontend Integration Guide]] — server-side vs client-side decision guide
- [[wiki/payloadcms/document-views|Document Views]] — `livePreview` document view key and tab config
- [[wiki/payloadcms/configuration|Payload Config — Overview]] — `admin.livePreview` root config
- [[wiki/payloadcms/localization|Localization]] — `locale` arg in dynamic URL function
