Skip to main content

Toolbar

In edit-md mode a toolbar offers inline formatting (bold, italic, strikethrough, inline code) plus Insert table and Insert drawing. Hide it with toolbar={false}.

Loading editor

Extending the toolbar

Pass a function as toolbar. It receives the default button groups (format, insert, history) and returns the toolbar to render. Use MarkdownEditor.ToolbarButton for your own buttons so they match the built-ins and keep the editor selection when clicked.

<MarkdownEditor
value={value}
onChange={setValue}
toolbar={(items) => (
<MarkdownEditor.Toolbar>
{items.format}
<MarkdownEditor.ToolbarDivider />
{items.insert}
<MarkdownEditor.ToolbarDivider />
{items.history}
<MarkdownEditor.ToolbarButton label="Save" onClick={save}>
💾
</MarkdownEditor.ToolbarButton>
</MarkdownEditor.Toolbar>
)}
/>

MarkdownEditor.Toolbar hides itself in view / edit-raw / readOnly. The undo / redo buttons (items.history) are only in the default layout through this function form; the plain toolbar={true} layout is format + insert.

This example reorders the groups (history first) and adds a copy button:

Loading editor

Placing your own toolbar (compound components)

When the toolbar must live somewhere else in your layout (an app bar, a panel header), compose the editor from its parts. Everything under MarkdownEditor.Root shares one editor instance, so the toolbar can sit anywhere in that subtree.

<MarkdownEditor.Root value={value} onChange={setValue}>
<header className="app-bar">
<MarkdownEditor.Toolbar>
<MarkdownEditor.FormatButtons />
<MarkdownEditor.ToolbarDivider />
<MarkdownEditor.InsertButtons />
</MarkdownEditor.Toolbar>
<MyAppButtons />
</header>
<MarkdownEditor.Content placeholder="Write…">
<MarkdownEditor.Outline />
</MarkdownEditor.Content>
</MarkdownEditor.Root>
PartRole
RootLexical composer + plugins. Takes value, onChange, mode, readOnly, autoFocus, className, measure, defaultBlockWidth, drawingStyle
ContentThe editable surface. Takes placeholder, foldable; children are docked sidebars
ToolbarContainer; renders the default groups when empty
FormatButtons, InsertButtons, HistoryButtonsBuilt-in groups
ToolbarButton, ToolbarDividerPrimitives for your own items
OutlineTable-of-contents sidebar

The same parts are exported at the top level (Toolbar, ToolbarButton, ToolbarDivider, FormatButtons, InsertButtons, HistoryButtons) for code that prefers named imports.

Here the toolbar sits in an app bar next to a Save button, and the outline is docked inside Content:

Loading editor

Headless: useMarkdownEditor()

For fully custom UI (buttons in your own design system, keyboard shortcuts, status bars), call the hook from any component rendered under MarkdownEditor.Root (also available as MarkdownEditor.useEditor):

import { useMarkdownEditor } from '@zuilib/text-editor'

function BoldButton() {
const { activeFormats, toggleFormat } = useMarkdownEditor()
return (
<MyButton pressed={activeFormats.has('bold')} onClick={() => toggleFormat('bold')}>
B
</MyButton>
)
}

It returns a MarkdownEditorApi:

MemberTypeNotes
editorLexicalEditorThe underlying instance, for dispatching your own commands
activeFormatsReadonlySet<TextFormatType>Formats active at the selection (bold, italic, underline, strikethrough, code)
toggleFormat(format)
insertTable(options?){ rows?, columns?, width? }width defaults to defaultBlockWidth.table; when given it is written explicitly, even full
insertDrawing(options?){ width? }Same rule with defaultBlockWidth.drawing
blockWidth / setBlockWidth(width)BlockWidth | nullWidth of the block containing the selection: the focused drawing, or else the table the caret is in; null outside both
tableDensity / setTableDensity(density)TableDensity | nullnull outside tables
tableCellTableCellPosition | null{ row, column, rows, columns, hasHeader } for the selection's cell
insertTableRow(position?)'above' | 'below' (default below)Never inserts above the header row: above there inserts right below it
deleteTableRow()No-op on the last row; deleting the header promotes the next row
insertTableColumn(position?)'before' | 'after' (default after)
deleteTableColumn()No-op on the last column
canUndo, canRedo, undo(), redo()
tableWidth / setTableWidthDeprecated table-only aliases of blockWidth / setBlockWidth

Insertions and deletions move the caret into whatever was created. The rules behind the table members are in Tables.

Every button below is a plain <button> driven by the hook; the status on the right reads activeFormats and tableCell:

Loading editor