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

Editor

Unstyled parts, hooks, and types for @createcms/react/editor.

The headless editor primitive: schema, state, form and preview layer. Unstyled; styling happens in the consumer's wrapper components (registry). For styled chrome, see Editor UI and the Visual editor guide.

import { Editor, createEditor, useEditor } from '@createcms/react/editor';

Usage

Editor.Root holds the store; Editor.Form renders every declared property of one block as a complete field. Kinds without a built-in control (image, reference, link) come from the fields map:

import type { AnyEditorSchema } from '@createcms/react/editor';
import type { BlockTreeNode } from '@createcms/core';

import { Editor, useFields } from '@createcms/react/editor';

import { MediaField } from './media-field';

function PageForm({
  schema,
  tree,
}: {
  schema: AnyEditorSchema;
  tree: BlockTreeNode;
}) {
  return (
    <Editor.Root
      schema={schema}
      defaultValue={tree}
      fields={{ image: MediaField }}
    >
      <Editor.Form blockId={tree.blockId} />
    </Editor.Root>
  );
}

The same form by hand, for a custom layout: map over useFields(blockId) and compose the parts yourself.

function BlockFields({ blockId }: { blockId: string }) {
  const fields = useFields(blockId);
  return fields.map(({ key }) => (
    <Editor.Field key={key} blockId={blockId} name={key}>
      <Editor.FieldLabel />
      <Editor.FieldControl />
      <Editor.FieldDescription />
      <Editor.FieldError />
    </Editor.Field>
  ));
}

Uncontrolled (key resets): schema, defaultValue, genId, userId and fields are read once at mount; render with a different key to load another document. onChange/onSave are read fresh on every call, so inline handlers are fine.

Editor.FieldControl accepts render only; it does not take className.

Parts

PartDefault elementPropsData attributes
Editor.Rootnone (provider)schema (required), defaultValue (required), onChange, onSave, genId, userId, fields, childrennone
Editor.FielddivblockId (required), name (required), disabled, render, div propsdata-kind, data-required, data-invalid, data-disabled, data-focused
Editor.FieldLabellabelrender, label props (children replaces the spec's label)data-required, data-invalid, data-disabled
Editor.FieldControlthe resolved control (see below)rendernone
Editor.FieldDescriptionprender, p props (children replaces the spec's description)none
Editor.FieldErrorp with role="alert", rendered while invalidrender, p props (children replaces the joined messages)data-invalid
Editor.FormdivblockId (required), disabled, autoScroll, render, div propsdata-block-type, data-block-id; each named group is a fieldset[data-group] with a legend
Editor.Previewdivrender (required, tree callback), debounceMs, div propsdata-stale while a version change is pending
Editor.FramePreviewdiv wrapping two iframesrender (required, async compiler), debounceMs, selectable, resolveAnchor, onIssues, onError, sandbox, title, div propsdata-loading, data-stale, data-error, data-kind (html or blob)
Editor.OutlineItemdiv with role="treeitem"blockId (required), onDelete, render, div propsdata-selected, data-depth, data-has-children, data-block-id, data-block-type
Editor.AddBlockbuttontype (required), parentId, index, render, button props (children replaces the palette label)data-block-type

Editor.Preview subscribes to the store version and passes the raw store tree (getTree()) to render after a delay (default PREVIEW_DEBOUNCE_MS, 100 ms, then one animation frame; debounceMs={0} still waits one frame).

Hooks

HookReturnsNotes
useEditorContextEditorContextValueThrows when used outside Editor.Root. Internal-facing.
useEditorSelectorT (from selector(state, store))Subscribes through useStoreSelector; returns the previous reference when the selected value is shallow-equal (shallowEqual).
useEditorStoreEditorStoreNo subscription: imperative access to the enclosing Editor.Root's store.
useEditorEditorApi (no args) or T (with a selector)No-arg form returns a stable object ({ ...store, schema, userId, store, scrollTo }); with a selector, a reactive slice. scrollTo(blockId, opts?) scrolls the registered form, or [data-block-id] inside opts.container when that option is set (no registry fallback). Returns false when the target is missing.
useAnyBlockAnyBlockHandle | nullnull for an unknown or null id; the handle is stable while the node is unchanged.
useAnyFieldAnyFieldHandleRe-renders only when that property's value or the node's type changes.
useFieldsSchemaField[]The block's property specs in schema order; [] for an unknown block; stable array identity.
useChildrenreadonly ChildRef[]{ id, type, index } in order; same array reference until ids or types change.
useBlockActionsBlockActionsPlacement-gated add/remove/duplicate/moveUp/moveDown plus canMoveUp, canMoveDown, canHaveChildren, allowedChildTypes.
useSelectionUserSelectionDefaults to the enclosing editor's user.
useHistoryHistoryApi{ canUndo, canRedo, undo, redo }.
useEditorKeyboardvoiduseEditorKeyboard(scopeRef, { delete?, escape? }). Bubbling keydown on the document, ignored unless the target is inside scopeRef.current; a consumer onKeyDown that preventDefaults skips the built-in handling. Undo/redo always; Delete/Escape opt-in. Throws outside Editor.Root.
useSaveSaveApi{ dirty, saving, save, markSaved }.
useDirtybooleanShorthand for useSave().dirty.
usePalettePaletteItem[]Every insertable block type, memoised per schema.
useMissingRequiredMissingRequiredField[]Every required property left empty across the document (blocks and root); memoised per nodes identity.
useFieldContextFieldContextValueThe enclosing Editor.Field's spec, value, setValue, ids, errors, flags; throws outside Editor.Field.

createEditor

createEditor({ schema }) binds Editor.Root, Preview, FramePreview and every hook above (including useEditorKeyboard) to one schema: Root has schema pre-set and defaultValue typed as TreeOf<typeof schema>; Preview's render callback receives TreeOf<typeof schema>; FramePreview's render callback receives TreeOf<typeof schema> plus { signal }; useBlock/useField narrow on the schema's declared block types; useChildren returns ChildRefOf<S>[] and useBlockActions returns TypedBlockActions<S>; add and usePalette resolve to never for a schema without statically known blocks. Every returned hook first checks that the enclosing Editor.Root uses the SAME schema object (===) and throws otherwise.

TypeDescription
TreeOf<S>The tree of S as getBlockTree delivers it (raw mode).
BlockTypeOf<S>The block type names of S; never for a block-less or dynamic schema.
BlockPropsOf<S, K>The property values of block K (or 'root').
RootPropsOf<S>BlockPropsOf<S, 'root'>.
BlocksOf<S>The statically known blocks of S; {} when none are declared.
PropsSpecOf<S, K>The property specs of block K (or 'root').
PropsOf<TSpec>The raw property values of a spec record.
PropValueOf<TSpec, P>The value type of one property P in TSpec.
BlockHandle<K, TSpec>A typed block handle for block type K with property specs TSpec.
BlockHandleOf<S>The discriminated union over every block handle of S, plus the root handle.
FieldHandle<V, Spec>A typed field handle: value, spec and setter of one property.
FieldHandleOf<S, K, P>FieldHandle for property P of block type K (or 'root') in S.
TypedEditorApi<S>useEditor()'s return type, with add restricted to S's block types.
TypedAddOptions<S, K>add's options for block type K: parentId, optional index, optional properties.
TypedPaletteItems<S>PaletteItem[] with type narrowed; never when S has no static blocks.
ChildRefOf<S>ChildRef with type narrowed to S's block types.
TypedBlockActions<S>BlockActions with add and allowedChildTypes restricted to S's block types.
EditorTypes<S>Phantom bag of the derived types (typeof editor.types.tree, …); {} at runtime.
EditorFactory<S>What createEditor returns.
CreateEditorOptions<S>createEditor's options: { schema: S }.

Schema helpers

Layer 1 of the editor primitive: pure functions over an EditorSchema (a CollectionDefinition). No React, no DOM.

FunctionSignatureNotes
getPlacement(schema) => PlacementIndexSame rules as core's buildPlacementIndex.
canPlace(index, childType, parentType) => boolean'root' for the top level; container gate, then accepts/excludes.
allowedChildTypes(index, parentType) => string[]Definition order; [] for a non-container.
defaultValuesFor(def, { fillDefaults? }) => Record<string, unknown>Declared defaultValues only (core semantics); fillDefaults adds false/0/first option/''/[].
propertiesOf(schema, blockType) => Record<string, BlockProperty>'root' → root fields; unknown → {}.
groupFields(properties) => FieldGroup[]Named groups first-appearance, null bucket last.
paletteItems / groupPaletteItems(schema) => PaletteItem[] / (items) => PaletteGroup[]Insertable block types (+ grouping).
isEmptyValue(spec, value) => booleanBlank string, empty list, link without target = empty; 0/false are values.
validateField(spec, value) => FieldError[]Client pre-check mirroring core's constraints; server stays authoritative.
missingRequired(schema, nodes) => MissingRequiredField[]Save/Publish gate over blocks + root (type === 'root').
TypeDescription
EditorSchemaThe editor's schema: a CollectionDefinition, generic over props/blocks.
AnyEditorSchemaThe wide form every runtime helper accepts.
FieldKindEvery kind a block/root property can have (BlockPropertyType plus list).
FieldSpecOf<K>The spec of one kind (e.g. FieldSpecOf<'select'> carries options).
FieldValueOf<K>The wide runtime value of one kind.
FieldValueMapKind → wide runtime value, the closed map FieldValueOf indexes.
SchemaFieldOne property of a block/root: its key and its spec.
PlacementIndexPrecomputed placement lookup for one schema (rules/containers/blockTypes).
PlacementRuleA resolved per-parent acceptance rule (only whitelist or except blacklist).
DefaultValuesOptionsOptions for defaultValuesFor (fillDefaults?).
FieldGroupFields under one group label (or the null ungrouped bucket).
PaletteItemA block type the palette can insert, derived from its definition.
PaletteGroupPalette items under one group label (or the null ungrouped bucket).
FieldErrorOne validateField finding: code, message, optional list index.
FieldErrorCodeThe closed set of validateField error codes.
MissingRequiredFieldA required property left empty on one node, from missingRequired.
MissingRequiredNodeThe minimum a node must carry for the missingRequired scan.

Store

Layer 2 of the editor primitive: a small getState/subscribe core with no React import. Rapid same-key updates coalesce into one undo step within COALESCE_MS. Dirty tracking uses stableHash.

Use createEditorStore to construct a store. Pure helpers: applyOp applies one op and returns { nodes, rootId, inverse }; createBlockId mints ids; flattenTree and serializeToTree convert between flat nodes and a tree; stableHash hashes a tree for dirty detection.

MethodSignatureNotes
getState() => EditorStoreStateSame object reference between changes.
subscribe(listener: () => void) => () => voidReturns an unsubscribe function.
getTree() => BlockTreeNodeSerialises nodes/rootId; memoised per version.
isDirty() => booleanCurrent hash vs. the hash at the last save/load.
load(tree: BlockTreeNode) => voidReplaces the tree; resets history and every user's selection; no onChange.
add(type: string, options: AddOptions) => string | nullSeeds declared defaults; null on an unknown parent or a disallowed placement.
update(id, patch, options?: UpdateOptions) => booleanMerge-patch; null deletes a key; false for an unknown id.
move(id, parentId, index) => booleanfalse on the root, an unknown id/target, a cycle, or a disallowed placement.
remove(id) => booleanRemoves the subtree; false for the root or an unknown id.
duplicate(id) => string | nullDeep-copies the subtree with fresh ids right after the original.
applyRemote(ops: readonly EditorOp[]) => ApplyRemoteResultApplies ops one by one; no history, no onChange; rejects are skipped.
undo / redo() => booleanfalse when there is nothing to undo/redo; closes the coalesce window.
select / hover(id: string | null) => voidWrites the local user's selection; select closes the coalesce window.
focus / setEditing(target: FieldRef | null) => voidWrites the local user's field ref; closes the coalesce window.
setUserSelection(userId, patch: Partial<UserSelection>) => voidSets any user's selection fields (for a later presence layer).
markSaved() => voidRebaselines the dirty hash to the current tree.
save(meta?: { message?: string }) => Promise<void>No-op when clean or without onSave; awaits onSave, then markSaved.
TypeDescription
AddOptionsadd's options: parentId, optional index, optional properties.
ApplyRemoteResult{ applied, rejected }, the ops applyRemote accepted/skipped.
ApplyResultapplyOp's success shape: { nodes, rootId, inverse }.
CreateEditorStoreOptionsOptions for createEditorStore: schema, initialTree, userId?, genId?, now?, getCallbacks?.
EditorCallbacks{ onChange?, onSave? } passed via getCallbacks().
EditorChangeThe onChange payload: { ops, version, getTree }.
EditorNodeOne flat tree node: { id, type, properties, parentId, childIds }.
EditorNodesRecord<id, EditorNode>.
EditorOpThe add/remove/move/update/load op union.
EditorStoreThe store's public shape (method-shorthand signatures, bivariant).
EditorStoreState{ rootId, nodes, selection, history, version, savedVersion, saving }.
FieldRef{ blockId, key }, what focus/editing point at.
HistoryEntryOne undo step: { ops, inverse, key, at }.
UpdateOptionsupdate's options: { coalesce? }.
UserSelectionOne user's { selected, hovered, focus, editing }.

Field helpers: defaultFieldControls, emptyListElement, toDatetimeLocal, fromDatetimeLocal.

Types

TypeDescription
EditorRootPropsProps of Editor.Root.
EditorContextValueWhat Editor.Root shares with its parts (schema, store, userId, fields, registerScrollTarget, scrollTo).
EditorKeyboardOptionsOptions for useEditorKeyboard: { delete?, escape? }.
EditorSelectorA useEditorSelector/useStoreSelector selector function.
EditorApiuseEditor()'s return type: the store's methods plus schema, userId, store, scrollTo.
EditorScrollToOptionsscrollTo options: ScrollIntoViewOptions plus optional container.
EditorPreviewPropsProps of Editor.Preview: render(tree), debounceMs?, div props.
EditorFramePreviewPropsProps of Editor.FramePreview: render(tree, { signal }), debounceMs?, selectable?, resolveAnchor?, onIssues?, onError?, sandbox?, title?, div props.
FramePreviewIssueOne onIssues finding: relative-url, missing-href, or preview-anchors.
FramePreviewAnchor{ blockId, key? } resolved from a click in a selectable frame.
FramePreviewKind'html' or 'blob', the last successfully displayed compile kind.
AnyBlockHandleuseAnyBlock's return type: a block's data, specs and setters.
AnyFieldHandleuseAnyField's return type: one property's value, spec and setter.
HistoryApiuseHistory's return type: { canUndo, canRedo, undo, redo }.
SaveApiuseSave's return type: { dirty, saving, save, markSaved }.
EditorFieldPropsProps of Editor.Field: blockId, name, disabled?, render?, div props.
EditorFieldLabelPropsProps of Editor.FieldLabel: render?, label props.
EditorFieldControlPropsProps of Editor.FieldControl: render?(props: AnyFieldControlProps).
EditorFieldDescriptionPropsProps of Editor.FieldDescription: render?, p props.
EditorFieldErrorPropsProps of Editor.FieldError: render?, p props.
EditorFormPropsProps of Editor.Form: blockId, disabled?, autoScroll?, render?, div props.
FieldControlProps<K>What a control of kind K receives (spec, value, onChange, ids, flags).
AnyFieldControlPropsThe wide form (spec: BlockProperty, value: unknown) render receives.
FieldControlsThe fields map of Editor.Root: one optional control component per kind.
FieldContextValueWhat Editor.Field shares with the parts below it (useFieldContext).
ListElementControlPropsWhat a list element's control receives (spec, value, index, ...).
ListElementRender(props: ListElementControlProps) => ReactElement | null, passed to list controls.
ChildRefOne child of a block: { id, type, index }.
BlockActionsStructural actions of one block (add/remove/duplicate/moveUp/moveDown).
EditorOutlineItemPropsProps of Editor.OutlineItem: blockId, onDelete?, render?, div props.
EditorAddBlockPropsProps of Editor.AddBlock: type, parentId?, index?, render?, button props.
OutlineItemStateuseRender state of Editor.OutlineItem.
AddBlockStateuseRender state of Editor.AddBlock ({ blockType }).

Data attributes

AttributeOnMeaning
data-kindEditor.Field, Editor.FramePreviewOn Field: the property kind (spec.type). On FramePreview: html or blob after the first successful display.
data-requiredEditor.Field, Editor.FieldLabelPresent when the spec is required.
data-invalidEditor.Field, Editor.FieldLabel, Editor.FieldErrorPresent while validateField reports at least one finding.
data-disabledEditor.Field, Editor.FieldLabelPresent when the field is disabled.
data-focusedEditor.FieldPresent while the store's focused field (local user) is this one.
data-staleEditor.Preview, Editor.FramePreviewPresent while a store version change is pending display.
data-loadingEditor.FramePreviewPresent while a compile for the current version is in flight.
data-errorEditor.FramePreviewPresent when the latest non-aborted compile failed.
data-block-typeEditor.Form, Editor.OutlineItem, Editor.AddBlockThe block type (root on a form for the root; the palette type on AddBlock).
data-block-idEditor.Form, Editor.OutlineItemThe block this form or row represents.
data-groupfieldset inside Editor.FormThe group label of the fields inside.
data-selectedEditor.OutlineItemPresent while this row is the local user's selected block.
data-depthEditor.OutlineItemHops from the root (root children are 1).
data-has-childrenEditor.OutlineItemPresent when the block has at least one child.

Keyboard

WhereKeysAction
Field controlsTab / Shift+TabNative tab order. Typing edits the value (one undo step via coalesce).
Editor.OutlineItemClickSelect the row.
Editor.OutlineItemArrowUp / ArrowDownSelect and focus the previous/next [role="treeitem"] in DOM order inside the closest [role="tree"] (fallback: the document).
Editor.OutlineItemAlt+ArrowUp / Alt+ArrowDownMove among siblings.
Editor.OutlineItemDelete / BackspaceRemove after onDelete (return false to keep).
Editor.OutlineItemEscapeClear the selection.
useEditorKeyboard(scopeRef)Ctrl/Cmd+ZUndo.
useEditorKeyboard(scopeRef)Ctrl/Cmd+Shift+Z or Ctrl+YRedo.
useEditorKeyboard(scopeRef, { delete: true })Delete / BackspaceRemove the selected block when the target is not an editable field.
useEditorKeyboard(scopeRef, { escape: true })EscapeClear the selection when the target is not an editable field.

ARIA

AttributeOnRule
aria-describedbythe control's focusable elementSpace-joined ids of the mounted description and, while invalid, the error. Omitted when empty.
aria-invalidthe control's focusable element"true" while validateField reports a finding; omitted when valid (never "false").
aria-requiredthe control's focusable element"true" when the spec is required; omitted otherwise. Native required is also set on built-in controls.
role="alert"Editor.FieldErrorRendered only while invalid.
htmlFor / idEditor.FieldLabel / controlLabel points at the control id from useId.
aria-selectedEditor.OutlineItemFollows the local user's selected block.
aria-levelEditor.OutlineItemDepth; root children are 1.
aria-expandedEditor.OutlineItem"true" when the block has children. No collapse state; a consumer overrides the attribute through props.
role="treeitem"Editor.OutlineItemDefault. Wrap the tree in [role="tree"].
data-requiredEditor.Field, Editor.FieldLabelPresent when the spec is required (styling hook; not a substitute for aria-required).

Focus

EventFocus
Tab into a fieldstore.focus({ blockId, key }) once (not on every keystroke). data-focused mirrors that.
Click a preview anchor in Editor.FramePreview (selectable)store.select / store.focus; the matching [data-editor-block] gets data-editor-focused and scrolls into view.
Editor.Form autoScrollThe form host scrolls into view (block: 'nearest') on each store focus change that targets that form.
Delete/Backspace on Editor.OutlineItemNeighbour row (next treeitem outside this subtree, else previous) is selected and focused; no neighbour clears the selection.
Alt+Arrow reorder on Editor.OutlineItemFocus is restored on the moved row after the DOM move.
useEditorKeyboard Delete/EscapeStore only; the hook does not move DOM focus.
Escape on Editor.OutlineItemSelection cleared; focus stays.

On this page