---
title: "Client mode"
description: "When to use client mode and its caveats."
requested_language: sk
language: en
translation_notice: "This page isn't translated yet"
url: https://docs.systhema.app/sk/next/payload/frontend/live-preview/client-mode
version: unreleased (main)
docs_index: https://docs.systhema.app/sk/next/llms.txt
---
> This page isn't translated yet. Showing English.


In `client` mode the preview iframe re-renders in the browser from the editor's unsaved form state, without a draft cookie or a save.

## When to use `client`

`client` mode needs no draft-mode cookie at all, so it live-updates on a locally built site in **every** browser **and** across a cross-origin admin. It also feels snappier (updates as you type). Switch to it for a cross-origin local build, or simply if you prefer real-time preview:

```ts
withSysthema(payloadConfig, { livePreview: { mode: 'client' } })
```

## `client` mode caveats

- **Register custom blocks and text states with `livePreview.clientSetup`.** `customBlocks` and `customTextStates` are registered during server-side config resolution, so in client mode a project's custom blocks render as nothing and its text states render unstyled — while the published page stays correct. Importing a registration module into your own views is not enough: Systhema's built-in views (`DefaultPageClientView`, `DefaultPostClientView`, `DefaultArchiveClientView`) cannot import consumer code, so any page on a built-in template would still be wrong.

  Point the option at a `'use client'` module whose module scope does the registering, and Systhema wraps every preview with it:

  ```tsx title="src/payload/previewClientSetup.tsx"
  'use client'
  import {
    registerSysthemaClientBlocks,
    registerSysthemaClientTextStates,
  } from '@systhemaui/payload/next/client'
  import { MyBlock, myBlockConverter } from './blocks/MyBlock'
  import { customTextStates } from './textStates'

  registerSysthemaClientBlocks([{ type: 'block', data: MyBlock, converter: myBlockConverter }])
  registerSysthemaClientTextStates(customTextStates)

  export default function PreviewClientSetup({ children }: { children: React.ReactNode }) {
    return <>{children}</>
  }
  ```

  ```ts
  withSysthema(payloadConfig, {
    livePreview: { mode: 'client', clientSetup: PreviewClientSetup },
  })
  ```

  It wraps rather than sitting beside the view deliberately: the registries are plain non-reactive objects, so a registration landing after the first render would never trigger a re-render. Wrapping makes the ordering structural instead of a race. In development, an unregistered block or text state now warns in the browser console rather than failing silently.

  In development the two lists are also **diffed by slug**, so a block you added to `payload.config.ts` and forgot in `clientSetup` is named in the browser console instead of rendering as `unknown node`:

  ```text
  [systhema] Custom block "gradientSection" is registered in payload.config.ts but not in
  livePreview.clientSetup, so it renders as "unknown node" in client-mode preview.
  Add it to the registerSysthemaClientBlocks call.
  ```

  The reverse case warns too — a `clientSetup` entry the server no longer defines (a renamed or removed block) will never render anything. Only slugs cross the boundary, only in development: a production build ships and checks nothing.

  Your converter modules run in the browser in this mode, so they must be client-safe — no `fs`, no server-only imports, and `RichText` imported from `@systhemaui/payload/next/client`.

- **Material Symbols and Apple emoji icons are fetched, not bundled.** Both packs store an identifier and resolve their markup from a digest only the server loads, so a browser-rendered tree would paint them empty. Client-mode preview batches the ids it could not resolve into one request to `GET <routes.api>/systhema/icons/resolve` — including icons the editor picks mid-session — and re-renders. Nothing to configure; see [Icons → Rendering icons in the browser](https://docs.systhema.app/sk/next/payload/editor/icons.md#rendering-icons-in-the-browser).

- **Videos are downgraded to `preload="none"` inside the preview** so a media-heavy page can't hold the iframe's `load` event open and leave Payload's skeleton up. Published pages keep whatever each block configured.

- **The live document is raw editor form state**, so server-injected values are absent until you save — most visibly the `_tier`/`_inCard` markers. A block you have just inserted or moved can render untiered in the overlay and correctly after publishing.

- **Client mode still needs draft mode for the initial document.** `/sys/preview` enables it regardless of mode; only the _update channel_ is cookie-free. Where the draft cookie cannot be set at all, a never-published document resolves to a 404 in the frame.

- **Custom blocks need their converters handed to the browser.** `customBlocks` are registered through the plugin option, which only runs on the server, so a client-mode preview renders them against an empty registry — each one logs `found <slug> block, but no converter is provided` and renders nothing, while the published page stays correct. Register them from a `'use client'` module your view imports:

  ```tsx
  'use client'
  import { registerSysthemaClientBlocks } from '@systhemaui/payload/next/client'
  import { MyBlock, myBlockConverter } from '@/payload/blocks/MyBlock'

  registerSysthemaClientBlocks([{ type: 'block', data: MyBlock, converter: myBlockConverter }])
  ```

  It is a no-op on the server, so it can never clobber the plugin's own registration.

- In the preview only, Lexical-nested form blocks, Google Maps embeds, and homepage link resolution rely on server context and fall back gracefully. Published pages render all of these normally.
- Autosave still runs (draft persistence) but no longer drives preview latency.
