⚠️ Work in progress — createCMS is pre-1.0 and not production-ready (not tested in production). Expect breaking changes.
createCMS
Guides

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/react

Install the registry items (see Editor UI for Field vs Form nesting):

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.json

The 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.

On this page