⚠️ 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. 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/react

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

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

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