Visual editor
Compose a live canvas, a form plus preview, or a form-only editor from @createcms/react and the docs registry.
This guide shows how to compose a styled editor from @createcms/react and
the docs registry. For a real createCMS backend, start with the one-install
editor-app block in Editor UI: it wraps
Editor.Root and Canvas.Root and wires useCmsDocument /
useCmsFieldSources. The chrome items below (editor-form, editor-canvas,
editor-shell, editor-email) do not wrap Root — compose those inside
Editor.Root.
Install
npm install @createcms/reactOne install (editor-app)
When you have a collection client, start with the CMS-wired page in Editor UI:
npx shadcn@latest add https://createcms.dev/r/editor-app.jsonBuilding-block chrome
Or install the chrome items individually. Field vs Form nesting is documented on Editor UI:
npx shadcn@latest add https://createcms.dev/r/editor-form.json
npx shadcn@latest add https://createcms.dev/r/editor-canvas.json
npx shadcn@latest add https://createcms.dev/r/editor-shell.json
npx shadcn@latest add https://createcms.dev/r/editor-email.jsonThe docs app hosts working demos composed from those registry items: live canvas, form only, form plus preview, and email split.
Form only
The primary form demo is CmsEditor mode="form": EditorShell mode="form"
(no palette, outline, inspector, or device toggles) plus FormSurface as
the single edit surface (page fields when nothing is selected). cmsFields
maps controls onto shadcn primitives (Input, Textarea, Select,
Checkbox). Document JSON is a Collapsible at the bottom of the shell,
not the primary UI.
'use client';
import { useEditor } from '@createcms/react/editor';
import * as React from 'react';
import { createDemoCmsClient } from '@/app/demo/_lib/demo-cms-client';
import { pageBlocks } from '@/app/demo/_lib/pages-blocks';
import { pages } from '@/app/demo/_lib/pages-schema';
import { PAGES_TREE } from '@/app/demo/_lib/pages-tree';
import { CmsEditor } from '@/components/editor-app';
import {
Collapsible,
CollapsibleContent,
CollapsibleTrigger,
} from '@/components/ui/collapsible';
function DocumentJson() {
const version = useEditor((state) => state.version);
const { getTree } = useEditor();
const tree = getTree();
void version;
return (
<Collapsible
data-slot="editor-document-json"
className="border-border shrink-0 border-t"
>
<CollapsibleTrigger className="text-muted-foreground hover:text-foreground flex w-full cursor-pointer items-center px-4 py-2 text-left text-xs font-medium">
Document JSON
</CollapsibleTrigger>
<CollapsibleContent>
<pre className="max-h-48 overflow-auto p-4 text-xs leading-relaxed">
{JSON.stringify(tree, null, 2)}
</pre>
</CollapsibleContent>
</Collapsible>
);
}
export function FormOnlyEditor() {
const client = React.useMemo(() => createDemoCmsClient(), []);
return (
<CmsEditor
client={client}
collection="pages"
rootId={PAGES_TREE.blockId}
branchId="demo"
schema={pages}
components={pageBlocks}
mode="form"
>
<DocumentJson />
</CmsEditor>
);
}Live canvas
Same Editor.Root wiring. EditorShell children are
Canvas.Root components={pageBlocks} with overlay rings, toolbar, insert,
drag handle, drop indicator, drag preview, and InlineText. Palette and
outline live in the shell's left Sidebar; the inspector is the right
Sidebar. Build pageBlocks from createBlocksMap with edit spread on
each block root.
'use client';
import { Editor, useEditor } from '@createcms/react/editor';
import { Canvas } from '@createcms/react/editor/canvas';
import * as React from 'react';
import { pageBlocks } from '@/app/demo/_lib/pages-blocks';
import { pages } from '@/app/demo/_lib/pages-schema';
import { PAGES_TREE } from '@/app/demo/_lib/pages-tree';
import { useDemoFieldSources } from '@/app/demo/_lib/sources';
import { useLocalDocument } from '@/app/demo/_lib/use-local-document';
import {
BlockToolbar,
DragPreview,
DropIndicator,
FieldRing,
HoverRing,
InlineText,
InsertButton,
Overlay,
SelectionRing,
} from '@/components/editor-canvas';
import {
CmsSourcesProvider,
cmsFields,
} from '@/components/editor-form';
import { EditorShell } from '@/components/editor-shell';
import {
Collapsible,
CollapsibleContent,
CollapsibleTrigger,
} from '@/components/ui/collapsible';
function DocumentJson() {
const version = useEditor((state) => state.version);
const { getTree } = useEditor();
const tree = getTree();
void version;
return (
<Collapsible
data-slot="editor-document-json"
className="border-border shrink-0 border-t"
>
<CollapsibleTrigger className="text-muted-foreground hover:text-foreground flex w-full cursor-pointer items-center px-4 py-2 text-left text-xs font-medium">
Document JSON
</CollapsibleTrigger>
<CollapsibleContent>
<pre className="max-h-48 overflow-auto p-4 text-xs leading-relaxed">
{JSON.stringify(tree, null, 2)}
</pre>
</CollapsibleContent>
</Collapsible>
);
}
export function PagesLiveCanvas() {
const sources = useDemoFieldSources();
const { onChange, onSave } = useLocalDocument(PAGES_TREE);
return (
<div className="flex min-h-0 flex-1 flex-col overflow-hidden">
<Editor.Root
schema={pages}
defaultValue={PAGES_TREE}
onChange={onChange}
onSave={onSave}
fields={cmsFields}
>
<CmsSourcesProvider sources={sources}>
<EditorShell>
<Canvas.Root
components={pageBlocks}
style={{ position: 'relative' }}
>
<Overlay>
<SelectionRing />
<HoverRing />
<FieldRing />
<BlockToolbar side="top" align="start" />
<InsertButton placement="between" type="richText" />
<DropIndicator />
<DragPreview />
<InlineText />
</Overlay>
</Canvas.Root>
</EditorShell>
<DocumentJson />
</CmsSourcesProvider>
</Editor.Root>
</div>
);
}A createCMS collection client can skip this local stand-in: install
editor-app from Editor UI and pass the client
plus your components map. CmsEditor already wires useCmsDocument as on
the Editor cms adapter reference page
(key={doc.key}, onChange={doc.onChange}, onSave={doc.save}, and
resolve={doc.resolve} on Canvas.Root).
Form plus preview
Root plus styled Form and Editor.Preview. Preview render receives the
raw tree. For HTML or email, use FramePreview with selectable and
render={async (tree, { signal }) => html}. For PDF, return a Blob from
FramePreview, or render a viewer in Preview.
'use client';
import { BlocksRenderer } from '@createcms/core/react/blocks';
import { Editor } from '@createcms/react/editor';
import { pageBlocks } from '@/app/demo/_lib/pages-blocks';
import { pages } from '@/app/demo/_lib/pages-schema';
import { PAGES_TREE } from '@/app/demo/_lib/pages-tree';
import { useDemoFieldSources } from '@/app/demo/_lib/sources';
import { useLocalDocument } from '@/app/demo/_lib/use-local-document';
import {
CmsSourcesProvider,
cmsFields,
Form,
} from '@/components/editor-form';
export function FormPreviewEditor() {
const sources = useDemoFieldSources();
const { saved, onChange, onSave } = useLocalDocument(PAGES_TREE);
return (
<div className="flex min-h-0 flex-1 flex-col">
<Editor.Root
schema={pages}
defaultValue={PAGES_TREE}
onChange={onChange}
onSave={onSave}
fields={cmsFields}
>
<CmsSourcesProvider sources={sources}>
<div className="grid min-h-0 flex-1 grid-cols-2 gap-4 p-4">
<Form blockId={PAGES_TREE.blockId} />
<Editor.Preview
render={(tree) => (
<BlocksRenderer blocks={pageBlocks} tree={tree} />
)}
/>
</div>
</CmsSourcesProvider>
</Editor.Root>
<pre className="border-border max-h-48 overflow-auto border-t p-4 text-xs">
{JSON.stringify(saved, null, 2)}
</pre>
</div>
);
}Email split
EditorEmail requires a render prop. It owns the styled form pane and the
Editor.FramePreview issues panel; the consumer only supplies render. Do
not set surface="frame".
'use client';
import { Editor } from '@createcms/react/editor';
import { emails } from '@/app/demo/_lib/email-schema';
import { EMAIL_TREE } from '@/app/demo/_lib/email-tree';
import { renderEmailHtml } from '@/app/demo/_lib/render-email';
import { useDemoFieldSources } from '@/app/demo/_lib/sources';
import { useLocalDocument } from '@/app/demo/_lib/use-local-document';
import {
CmsSourcesProvider,
cmsFields,
} from '@/components/editor-form';
import { EditorEmail } from '@/components/editor-email';
export function EmailSplitEditor() {
const sources = useDemoFieldSources();
const { saved, onChange, onSave } = useLocalDocument(EMAIL_TREE);
return (
<div className="flex min-h-0 flex-1 flex-col">
<Editor.Root
schema={emails}
defaultValue={EMAIL_TREE}
onChange={onChange}
onSave={onSave}
fields={cmsFields}
>
<CmsSourcesProvider sources={sources}>
<EditorEmail render={async (tree) => renderEmailHtml(tree)} />
</CmsSourcesProvider>
</Editor.Root>
<pre className="border-border max-h-48 overflow-auto border-t p-4 text-xs">
{JSON.stringify(saved, null, 2)}
</pre>
</div>
);
}Block component with edit
Spread edit.block and edit.field.<key> on consumer block markup:
import type { BlockComponentProps } from '@createcms/core/react/blocks';
function Hero({ properties, edit }: BlockComponentProps) {
return (
<section {...edit.block}>
<h1 {...edit.field.headline}>{properties.headline}</h1>
</section>
);
}On the published renderer, pass edit="preview" or NO_EDIT instead. See
React.
Unstyled
The unstyled API lives at @createcms/react/editor. Reference pages:
Editor,
Editor canvas, and
Editor cms adapter.