Editor UI
Install a CMS-wired editor page in one command, or copy styled chrome from the createCMS shadcn registry.
The createCMS docs site hosts a shadcn registry of copy-paste editor chrome. Start with editor-app for a complete CMS-wired page.
The chrome items (editor-form, editor-canvas, editor-shell, editor-email) do not wrap Editor.Root or Canvas.Root — compose them inside Editor.Root. For form-only, live canvas, and form-plus-preview layouts, see the Visual editor guide.
Prerequisites
Your app should already be initialized with shadcn (npx shadcn@latest init) and have @createcms/react installed:
npm install @createcms/reacteditor-app handles the CMS wiring (useCmsDocument and useCmsFieldSources). Pass fields={cmsFields} on Editor.Root and wrap with CmsSourcesProvider only when you compose chrome yourself — pass the result of useCmsFieldSources(client) into the provider.
Start with one install
editor-app copies a complete CMS-wired editor page in one shadcn add.
npx shadcn@latest add https://createcms.dev/r/editor-app.json'use client';
import { CmsEditor } from '@/components/editor-app';
export function PageEditor({
rootId,
branchId,
}: {
rootId: string;
branchId: string;
}) {
return (
<CmsEditor
client={cmsClient}
collection="pages"
rootId={rootId}
branchId={branchId}
schema={pages}
components={pageBlocks}
/>
);
}Copied files land in your shadcn components directory. Adjust the import if your aliases differ.
What CmsEditor includes:
- Document load and save (
useCmsDocument) - Field sources (
useCmsFieldSources,CmsSourcesProvider,fields={cmsFields}) EditorShelland canvas overlays- Loading skeleton, error + Retry (
doc.reload), and a conflict dialog onHEAD_MISMATCH(reload vssave({ force: true }))
mode="form" skips Canvas.Root and passes mode into EditorShell. Form mode hides device toggles, the palette/outline sidebar, Add block, and the inspector so FormSurface is the only edit surface. The form-only demo is this path: CmsEditor mode="form" with a Collapsible JSON dump as children of the shell, not a bare Editor.Root + Form composition.
Props
| Prop | Required | Default | Description |
|---|---|---|---|
client | yes | — | Top-level createCMS client (collection namespaces plus field sources). |
collection | yes | — | Collection name on client. |
rootId | yes | — | Document root. |
branchId | yes | — | Branch to load and save. |
schema | yes | — | Editor schema. |
components | yes | — | Canvas block map. |
templates | no | client.templates | Override for useCmsDocument template defaults. |
mode | no | 'canvas' | 'canvas' or 'form'. Form skips Canvas.Root and EditorShell hides canvas chrome (palette, outline, inspector, device toggle). |
className | no | — | Applied to the page root, loading skeleton, and error state. |
requireCommitMessage | no | false | Opens the shell commit-message dialog on save. |
children | no | — | Canvas: after the shell. Form: inside the shell surface under FormSurface (demo extras such as a JSON Collapsible). |
While developing the docs site locally, swap https://createcms.dev for http://localhost:4000.
When composing yourself
If you installed editor-app, you can stop here. CmsEditor already depends on editor-form, editor-canvas, and editor-shell. Install the items below only when you want to compose chrome yourself.
Field wrappers and cms controls
npx shadcn@latest add https://createcms.dev/r/editor-form.jsonCanvas overlay chrome
npx shadcn@latest add https://createcms.dev/r/editor-canvas.jsonSidebar-grade shell
npx shadcn@latest add https://createcms.dev/r/editor-shell.jsonEmail split layout
npx shadcn@latest add https://createcms.dev/r/editor-email.jsonUsage
Compose these parts inside <Editor.Root schema={…}>. Do not nest styled Field inside styled Form.
Cms field controls
import { Editor } from '@createcms/react/editor';
import { useCmsFieldSources } from '@createcms/react/editor/cms';
import {
CmsSourcesProvider,
cmsFields,
Form,
} from '@/components/editor-form';
const sources = useCmsFieldSources(cmsClient);
<Editor.Root schema={schema} fields={cmsFields}>
<CmsSourcesProvider sources={sources}>
<Form blockId={blockId} />
</CmsSourcesProvider>
</Editor.Root>Styled container. Wrap the block's fields with the styled Form shell. Inner fields remain the headless primitives from @createcms/react unless you pass fields={cmsFields} (shadcn Input / Textarea / Select / Checkbox and CMS pickers). FormSurface is a padded selected-block form for CmsEditor mode="form" and the form-only demo. Do not replace those primitives with custom input chrome.
import { Editor } from '@createcms/react/editor';
import { Form } from '@/components/editor-form';
<Editor.Root schema={schema}>
<Form blockId={blockId} />
</Editor.Root>Styled parts mapped by the consumer. Render each field with the styled Field, FieldLabel, and FieldControl parts. Do not nest these inside Form.
import { Editor } from '@createcms/react/editor';
import { Field, FieldLabel, FieldControl } from '@/components/editor-form';
<Editor.Root schema={schema}>
<Field blockId={blockId} name="title">
<FieldLabel />
<FieldControl />
</Field>
</Editor.Root>Shell and canvas. Place EditorShell (or its exported parts) and styled canvas parts inside Editor.Root. Pass Canvas.Root and overlay parts as children of EditorSurface. EditorShell wraps Canvas.Provider so palette items in the left sidebar can start drag sessions.
The items copy into your shadcn components directory. Adjust import paths to match your aliases.
editor-shell parts
The assembled EditorShell is a default composition of shadcn Sidebar (left palette + outline, right inspector), not a custom three-column grid. Cmd+B, icon-rail collapse, mobile sheet, and cookie persistence come from SidebarProvider.
| Export | Role | Composed from |
|---|---|---|
EditorProvider | TooltipProvider + SidebarProvider + Canvas.Provider + keyboard (⌘S, ⌘K, undo/redo) | shadcn sidebar, tooltip |
EditorToolbar | Icon buttons with Tooltip + Kbd, device ToggleGroup (canvas mode), save dirty-dot + spinner | button, tooltip, kbd, toggle-group, separator |
EditorPalette | SidebarGroup of Canvas.PaletteItem rows with grip affordance | sidebar, editor-canvas |
EditorOutline | Tree rows on SidebarMenuButton + Editor.OutlineItem, chevron collapse, type icon | sidebar, collapsible |
EditorInspector | Tabs (Block / Page), empty state, sticky header with Duplicate / Delete | tabs, editor-form |
EditorSurface | Device-width canvas column, or a scrolling form column in mode="form" | — |
EditorShell | Default assembly. mode="form" hides palette, outline, inspector, and device toggles. | all of the above |
useEditorChrome() reads device preview, add-block dialog, and save-dialog state from EditorProvider.
Compose a custom layout from the same parts:
import {
EditorInspector,
EditorOutline,
EditorPalette,
EditorProvider,
EditorSurface,
EditorToolbar,
} from '@/components/editor-shell';
import {
Sidebar,
SidebarContent,
SidebarInset,
SidebarRail,
} from '@/components/ui/sidebar';
<EditorProvider>
<Sidebar side="left" collapsible="icon">
<SidebarContent>
<EditorPalette />
<EditorOutline />
</SidebarContent>
<SidebarRail />
</Sidebar>
<SidebarInset>
<EditorToolbar />
<EditorSurface>{/* Canvas.Root */}</EditorSurface>
</SidebarInset>
<Sidebar side="right" collapsible="icon">
<SidebarContent>
<EditorInspector />
</SidebarContent>
<SidebarRail />
</Sidebar>
</EditorProvider>editor-canvas parts
| Export | Role |
|---|---|
Overlay | Portals overlay chrome into the canvas host |
SelectionRing | Selection outline with a block-type chip; motion respects reduced-motion |
HoverRing | Hover outline |
FieldRing | Focused field outline |
BlockToolbar | Icon buttons + tooltips (drag, move, duplicate, delete) when no children |
InsertButton | Between-block insert control |
DropIndicator | Drop line with endpoint caps; box variant for empty containers |
DragHandle | Grip handle for moving a block |
PaletteItem | Draggable palette row (used by EditorPalette) |
DragPreview | Pointer-following preview during a drag |
InlineText | Inline text editing glass |