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. Compose inside Editor.Root. Never wrap Root in a
snippet that lives in the registry.
Install
npm install @createcms/reactInstall the registry items (see Editor UI for Field vs Form nesting):
npx shadcn@latest add https://createcms.dev/r/editor-form.jsonnpx shadcn@latest add https://createcms.dev/r/editor-canvas.jsonnpx shadcn@latest add https://createcms.dev/r/editor-shell.jsonnpx 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
Local tree state with onSave that stores the latest tree. Pass
fields={cmsFields} and wrap with CmsSourcesProvider. No canvas.
'use client';
import { Editor } from '@createcms/react/editor';
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 FormOnlyEditor() {
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}>
<Form blockId={PAGES_TREE.blockId} />
</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>
);
}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. Build
pageBlocks from createBlocksMap with edit spread on each block root.
'use client';
import { Editor } from '@createcms/react/editor';
import { Canvas } from '@createcms/react/editor/canvas';
import { useSelection } 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 {
BlockToolbar,
DragHandle,
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';
function DragHandleInner() {
const selected = useSelection().selected;
if (!selected) return null;
return <DragHandle blockId={selected} />;
}
export function PagesLiveCanvas() {
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}>
<EditorShell>
<Canvas.Root
components={pageBlocks}
style={{ position: 'relative' }}
>
<Overlay>
<SelectionRing />
<HoverRing />
<FieldRing />
<BlockToolbar side="top" align="start" />
<InsertButton placement="between" type="richText" />
<DragHandleInner />
<DropIndicator />
<DragPreview />
<InlineText />
</Overlay>
</Canvas.Root>
</EditorShell>
</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>
);
}A createCMS collection client uses useCmsDocument as on the
Editor cms adapter reference page:
pass key={doc.key}, onChange={doc.onChange}, onSave={doc.save}, and
resolve={doc.resolve} on Canvas.Root when wiring a real backend.
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.