Toolbar and slash menu

The chrome, derived from the registry, and how to compose it.

ContentEditor.Toolbar with no children is one row of the everyday controls (variant="compact"): the block type as a menu naming the block at the selection, the marks and the link, the two lists, and every insertable block under one « Insert » menu. Undo, redo, indent and outdent are marked secondary by their features and stay on their keys. variant="full" lays every item out as its own control, grouped in a fixed order: history, text, block, list, insert, table.

An item is a Toggle when it knows whether it is active, a ghost Button otherwise, with a Tooltip carrying its label and its keys, and a Popover when it opens a form. An item that requires a capability the host did not wire (image upload) is absent, not disabled. ContentEditor.ToolbarMenu folds any run of items into one menu when you compose the row yourself.

The row scrolls sideways when the groups outgrow it, the way a phone's keyboard bar does; pass wrap to let the groups wrap onto several rows instead.

Compose the groups by hand for a narrower toolbar:

Writing inside your own app

Rich text with bold, italic and a  link . Type / for a block, or select text for the floating toolbar.
Every block on this page is one feature definition.
  • Lists, with Tab to nest
  • Tables, images, embeds
And a quote, for when someone else said it better.
const items = FEATURES.flatMap((feature) => feature.toolbar ?? []);
 
<ContentEditor.Toolbar>
	<ContentEditor.ToolbarGroup items={items.filter((item) => item.group === "text")} />
	<ContentEditor.ToolbarGroup items={items.filter((item) => item.group === "block")} />
</ContentEditor.Toolbar>

The floating toolbar

ContentEditor.FloatingToolbar shows the features' floating items above a text selection, flipped below when the top has no room. It follows the editor's own selection rather than the DOM's, so it stays over the selected text while the link form inside it holds focus.

The slash menu

Typing / opens the registry's slash items, filtered on the host's labels and each item's keywords, on the kit's Command. The menu is the slash feature: an editor built without it has none.

Single line

A subtitle with emphasis

mode="single-line" drops every block feature and holds one paragraph: Enter is swallowed, a seed of several blocks is folded into its first. createInlineContentFeatures() is the matching feature set.

API

ContentEditor.Root

PropTypeDefault
childrenReactNoderequired
featuresContentRegistry | readonly ContentFeature[]requiredThe features this editor offers. createContentFeatures returns the ones this package ships; add your own, drop what you do not want, reorder them. Either the list or a registry built from it.
onChange(value: Value) => voidrequiredCalled with the whole next document on every edit, keystrokes included.
valueValue | nullrequiredThe document being edited, plain serialisable data; null is empty.
classNamestring
generateNodeId(() => string)newContentNodeIdNode-id factory, injectable for deterministic tests.
labelsContentEditorLabelsInput
modeContentEditorMode"block"block is the full editor. inline drops every block feature (marks and links only). single-line is inline with one paragraph and Enter as a submit rather than a break.
onUploadImage((file: File) => Promise<ContentUploadedImage>)Delegated image upload: receives the picked file, resolves with its url. Omit to hide every image affordance.
readOnlybooleanfalse
themeContentEditorThemeInput

Plus the DOM props of the element it renders. Source: content-editor/components/root.tsx.

ContentEditor.Canvas

PropTypeDefault
placeholderstringShown in the empty document; defaults to the chrome.placeholder label.
asElementType<any, keyof IntrinsicElements>
autoFocusOnEditablebooleanAutofocus when it becomes editable (readOnly false -> readOnly true)
decorate((options: { editor: PlateEditor; entry: NodeEntry; }) => TRange[]) | null
disabledboolean
disableDefaultStylesboolean
onDOMBeforeInput((event: InputEvent) => void)
renderChunkRenderChunkFn
renderEditable((editable: ReactElement<any, string | JSXElementConstructor<any>>) => ReactNode)R enders the editable content.
renderElementRenderElementFn
renderLeafRenderLeafFn
renderPlaceholder((props: { attributes: { contentEditable: boolean; 'data-slate-placeholder': boolean; ref: (instance: any) => void | (() => VoidOrUndefinedOnly); style: CSSProperties; dir?: "rtl" | undefined; }; children: any; }) => Element)
renderTextRenderTextFn
rolestring
scrollSelectionIntoView((editor: Editor, domRange: Range) => void)
styleCSSProperties

Plus the DOM props of the element it renders. Source: content-editor/components/canvas.tsx.

ContentEditor.Toolbar

PropTypeDefault
childrenReactNodeCompose groups by hand; without children every registry item is shown.
classNamestring
variant"compact" | "full""compact"compact (the default) is one row of the everyday controls: the block type as a menu, the marks, the lists, everything insertable under one « Insert » menu; undo, redo and indent stay on their keys. full lays every item out as its own control, grouped.
wrapbooleanfalseWrap the groups onto several rows instead of scrolling one row sideways. Off by default: on a phone, wrapped groups leave a ragged block of rows and hairlines above the document, while one row scrolls the way a phone keyboard bar does.

Plus the DOM props of the element it renders. Source: content-editor/components/toolbar.tsx.

ContentEditor.FloatingToolbar

PropTypeDefault
classNamestring

Plus the DOM props of the element it renders. Source: content-editor/components/floating-toolbar.tsx.