Document Editor

Pro

A blank page people write in: a title, a slash menu for blocks, @ mentions, a bar over every selection, and to-dos, toggles, images, tables and code. Built on Tiptap.

MMara Okonkwo
0 words · 0 min read

Installation#

# One-time setup (tokens + lib helpers)npx koalaui-cli@latest init# Activate your Pro license (once per machine)npx koalaui-cli@latest login <key># Add this component (its dependencies come along)npx koalaui-cli@latest add document-editor

Usage#

This example is part of Koala UI Pro

The preview is yours to explore. Its code comes with a Pro license, paid once and used in every project.

Have a license? Unlock it here

Anatomy#

import {  DocumentEditor,  DocumentEditorCanvas,  DocumentEditorHeader,  DocumentEditorIcon,  DocumentEditorHeaderActions,  DocumentEditorHeaderAction,  DocumentEditorTitle,  DocumentEditorMeta,  DocumentEditorMetaItem,  DocumentEditorContent,  DocumentEditorBubbleMenu,  DocumentEditorBubbleButton,  DocumentEditorBubbleSeparator,  DocumentEditorTableMenu,} from "@/components/ui/document-editor"<DocumentEditor>  <DocumentEditorCanvas>    <DocumentEditorHeader>      <DocumentEditorHeaderActions>        <DocumentEditorHeaderAction />      </DocumentEditorHeaderActions>      <DocumentEditorIcon />      <DocumentEditorTitle />      <DocumentEditorMeta>        <DocumentEditorMetaItem />      </DocumentEditorMeta>    </DocumentEditorHeader>    <DocumentEditorContent />    <DocumentEditorBubbleMenu>      <DocumentEditorBubbleSeparator />      <DocumentEditorBubbleButton />    </DocumentEditorBubbleMenu>    <DocumentEditorTableMenu />  </DocumentEditorCanvas></DocumentEditor>

Title and header#

The title is a field of its own, not the first block, so it can never be deleted or turned into a list. It grows with its text and moves into the body on Enter. Above it, DocumentEditorHeaderActions rest hidden and fade in on hover,DocumentEditorIcon holds the page emoji, and DocumentEditorMeta lays out chips under the title.

MMara Okonkwo

Width#

width sets the measure: page holds a line to 65 characters, wide opens to 900px for tables and images, and full fills the container. Switching slides the measure instead of jumping.

Slash menu#

With slashCommandExtension(), a / at the start of a block or after a space opens the block menu at the caret. Typing filters it by title and keyword, arrows move, Enter or Tab inserts. Markdown shortcuts work too: #, -, [], >, ```.

Every block it offers, painted with SlashMenuList, the same list on its own for a picker of your own.

Basic
Text
Heading 1#
Heading 2##
Heading 3###
Quote>
Divider---
Lists
Bulleted list-
Numbered list1.
To-do list[]
Toggle
Media
Image
Table
2 columns
3 columns
Link card
Advanced
Code block```
Table of contents

commands decides what the menu offers: filter the registry, rename it, or add a BlockCommand of your own.

Bubble menu#

Select text and the bar rises over it. Highlight offers four colours mixed from the status tokens. onComment adds a Comment button, and children land at the end of the bar: the slot for an Ask AI action.

Select text to open the bar

To-do list#

Type [] and a space. Items nest with Tab, and a checked one fades to the muted ink with a line through it.

Toggle#

A summary line that opens to reveal the blocks inside. Whether it is open is saved with the document.

Image#

An empty image block offers upload and a link field; pasting or dropping a picture anywhere inserts one. Select an image to align it, drag its edges to resize it, and add a caption. Pass onUpload to store files instead of previewing them locally.

Table#

A stroked grid, a faint header row and resizable columns. Selected cells take an ink wash, and DocumentEditorTableMenu adds grips on the hovered row and column, bars that add a row or a column, and the table's options at its corner.

Columns put two or three stacks of blocks side by side and fold to one on phones. The contents block lists the page's headings live and jumps to them. A link card turns a URL into a calm card with its title and domain; paste a bare link on an empty line to make one.

Mentions#

Type @ for a list of people and pages, grouped, filtered as you type. The pick lands as a quiet chip: an @ before a person, the emoji before a page. A click on a page chip calls onOpen.

Code block and divider#

The code block shows its language and a copy button on hover. A divider takes a soft ring when selected, so it can be moved or deleted like any block.

Read-only#

editable={false} renders the same page for reading. The bubble and table menus stay out, the title is read-only and checkboxes stop toggling, so the composition can stay as it is.

Density#

compact suits a document inside an app screen, comfortable a page given the whole window. It scales the title, the body, the headings and every gap between blocks.

compact

comfortable

Labels#

Each part takes its strings where it is set up: the root's labels, the bubble's labels, the blocks' labels and the slash menu's commands and emptyLabel.

Word count#

useDocumentEditorStats returns the words, characters and minutes of reading of the live document, for a status bar anywhere inside the editor.

0 words · 0 min read

API reference#

DocumentEditor

The root: creates the Tiptap editor and shares it, the labels and the editable state with every part through context. Built in: StarterKit (headings 1 to 3, links that autolink and do not open on click, an ink drop line), the placeholder, smart typography, multicolour highlight and text alignment. Forwards native <div> props (except onChange and defaultValue); every part accepts className, merged last.

PropTypeDefaultDescription
defaultValueContent-Initial document: an HTML string or Tiptap JSON. Uncontrolled; key the editor by document id to load another one.
onChange(editor: Editor) => void-Called on every edit with the live editor. Read editor.getJSON() or editor.getHTML() there, ideally debounced.
editablebooleantrueSet false to render the document read-only. The bubble and table menus hide, the title turns read-only and checkboxes stop toggling. Toggles without remounting.
extensionsExtensions-More Tiptap extensions, after the built-in ones: slashCommandExtension(), blockHandleExtension(), ...documentEditorBlocks() and your own. Read when the editor mounts.
width"page" | "wide" | "full""page"The text measure: 65 characters, 900px, or the whole container with a deeper gutter. Changing it slides the measure.
density"compact" | "comfortable""compact"Type size and spacing: compact sets the body at 15px and the title at 34px, comfortable at 16px and 40px with more air between blocks. Falls back to the nearest DensityProvider.
labelsPartial<DocumentEditorLabels>-Rewords the root's strings: placeholder, titlePlaceholder, headingPlaceholder (followed by the level) and slashEmpty. Missing keys keep the English defaults.
Data attributeDescription
data-widthThe resolved width: page, wide or full.
data-densityThe resolved density: compact or comfortable.
CSS variableDescription
--doc-measureThe text column: 65ch (page), 56.25rem (wide) or 100% (full).
--doc-gutterSide padding of the canvas: 16px on phones, 56px (compact) or 64px (comfortable) from sm, 96px for full from lg.
--doc-topSpace above the header: 64px compact, 96px comfortable.
--doc-textBody size: 15px compact, 16px comfortable.
--doc-lineBody line height: 1.6 times the body size in compact, 1.65 in comfortable.
--doc-flowSpace between blocks.
--doc-itemSpace between list items and between paragraphs of a quote.
--doc-h1, --doc-h2, --doc-h3Space above each heading level.
--doc-after-headingSpace between a heading and the block under it.
--doc-after-titleSpace between the header and the body.
--doc-hrSpace above and below a divider.

DocumentEditorCanvas

The page: centres the measure, pads the gutter and the top, and ends in a tail you can click to keep writing (it adds an empty paragraph if the last block is not one). Forwards <div> props.

DocumentEditorHeader

The block above the body that holds the icon, the header actions, the title and the meta row. Its hover and focus reveal the header actions. Forwards <div> props.

DocumentEditorIcon

A 72px emoji button for the page icon. Wire it to your own picker (a Popover, the Emoji Picker). Forwards <button> props; type defaults to "button".

DocumentEditorHeaderActions

A row of quiet actions above the title (Add icon, Add cover). Hidden at rest, it fades in while the header is hovered or focused, and always shows on touch screens. Forwards <div> props.

DocumentEditorHeaderAction

One header action: muted text with an optional 16px icon and a 40px hit area. Forwards <button> props.

DocumentEditorTitle

The title, a textarea that grows with its text and never takes a line break. Enter moves into the body (text after the caret goes with it), ArrowDown at the end moves to the first block, and ArrowUp at the top of the body comes back. Forwards <textarea> props.

PropTypeDefaultDescription
valuestring-Controlled title.
defaultValuestring""Initial title when uncontrolled.
onValueChange(value: string) => void-Called with the new title; pasted line breaks become spaces.
placeholderstringlabels.titlePlaceholderShown while the title is empty ("Untitled").
aria-labelstring"Title"The accessible name of the field.

DocumentEditorMeta

A wrapping row of chips under the title: the date, the author, a status. Its gap above follows the density. Forwards <div> props.

DocumentEditorMetaItem

One chip: a soft fill, muted text in tabular figures, a 14px icon or an xs avatar tucked to its edge. As a link or button it gains a hover fill, a focus ring and a 40px hit area.

PropTypeDefaultDescription
asChildbooleanfalseRenders your element (a link, a button) through Radix Slot instead of a <span>.

DocumentEditorContent

The writing area: Tiptap's EditorContent with the document typography (foreground headings, body in the reading ink, lists, quotes, links, inline code, highlights). Forwards <div> props.

DocumentEditorBubbleMenu

The bar over a text selection: Turn into, bold, italic, underline, strikethrough, inline code, link (⌘K opens it from the keyboard too), highlight, then an optional Comment and your own buttons. Hidden in code blocks and while read-only. It is the floating Toolbar: same surface, controls, roving focus and tooltips, and it forwards the Toolbar props (size, className…).

PropTypeDefaultDescription
onComment(editor: Editor) => void-Adds a Comment button and calls it with the editor while the selection is still live.
childrenReact.ReactNode-Your own controls after the built-ins, such as an Ask AI DocumentEditorBubbleButton.
labelsPartial<DocumentEditorBubbleMenuLabels>-Rewords the bar: toolbar, turnInto, bold, italic, underline, strike, code, link, linkPlaceholder, applyLink, removeLink, highlight, highlightNone, comment.
optionsBubbleMenuProps["options"]{ placement: "top", offset: 8 }Floating UI options for Tiptap's BubbleMenu (placement, offset, flip, shift), merged over the defaults.
Data attributeDescription
data-side"top" or "bottom": where the bar sits against the selection, so it grows from that edge.

DocumentEditorBubbleButton

A ToolbarButton for your own bubble controls: a tooltip with its shortcut and a pressed state. Takes every ToolbarButton prop (caret, variant…).

PropTypeDefaultDescription
tooltip*string-The accessible label and tooltip.
shortcutstring-A keycap shown in the tooltip. Display only.
pressedboolean-Marks the control active and sets aria-pressed.
Data attributeDescription
data-pressedPresent when pressed.

DocumentEditorBubbleSeparator

A ToolbarSeparator between groups of bubble controls.

DocumentEditorTableMenu

The table's controls, shown while a cell is hovered or holds the caret: a grip on the column (insert left or right, sort, delete) and on the row (insert above or below, header row, delete), bars on the right and bottom edges that add a column or a row, and an options button at the corner (header row, header column, fit to width, delete table). Needs the table block.

PropTypeDefaultDescription
labelsPartial<TableMenuLabels>-Rewords the controls: columnOptions, rowOptions, tableOptions, addColumn, addRow, addRowBefore, addRowAfter, deleteRow, headerRow, headerColumn, addColumnBefore, addColumnAfter, sortAscending, sortDescending, deleteColumn, fitWidth, deleteTable.

documentEditorMentions(options)

The @ list and the mention chip. Renders as span[data-type="mention"] with data-id, data-kind, data-label and data-icon, and parses back.

PropTypeDefaultDescription
items*(query: string) => MentionItem[]-Everything that can be mentioned: { id, label, kind: "person" | "page", avatar?, icon? }. Filtered by the start of any word.
onOpen(item: MentionItem) => void-A click on a page chip.
onSelect(item: MentionItem) => void-Called after a mention is inserted.
labelsPartial<DocumentEditorMentionLabels>-people, pages, empty, untitled.
charstring"@"The trigger character.

documentEditorBlocks(options)

Returns the rich block extensions: taskList, details (toggle), image, table, columns, tableOfContents, linkCard and codeBlockChrome (the code block's language label and copy button, and the divider's selected state). Spread it into extensions.

PropTypeDefaultDescription
onUpload(file: File) => Promise<string>URL.createObjectURLTurns a picked, pasted or dropped image into a URL. The default only previews locally; pass your storage upload.
excludeDocumentEditorBlockName[][]Leaves blocks out by name. Their slash commands disappear with them.
labelsPartial<…>-The blocks' strings. Image: empty, upload, urlPlaceholder, embed, caption, resize, alignLeft, alignCenter, alignRight, fitWidth, remove. Toggle: expand, collapse. Code: copyCode, copied.
linkCardLabelsPartial<LinkCardLabels>-The link card's strings: empty, urlPlaceholder, embed, open, title.
tocLabels{ empty?: string; label?: string }-The contents block's empty message and the accessible name of its list.

slashCommandExtension(options)

The / menu. Typed at the start of a block or after a space (never in code), it lists blocks grouped as Basic, Lists, Media and Advanced, filters as you type, runs on Enter or Tab and gives up after three characters with no match.

PropTypeDefaultDescription
commands(editor: Editor) => BlockCommand[]getBlockCommandsThe commands offered. Filter, reorder, rename or add your own.
emptyLabelstring"No blocks match"Shown while nothing matches.
charstring"/"The character that opens the menu.

BlockCommand

One entry of the block registry. BLOCK_COMMANDS is the full list; getBlockCommands(editor) keeps the ones whose node the editor has. The same list feeds the slash menu and both Turn into menus.

PropTypeDefaultDescription
id*string-Stable key: text, heading1, quote, divider, bulletList, taskList, toggle, image, table, codeBlock…
title*string-The row label.
descriptionstring-One short line, shown as the row's hint.
group*"basic" | "lists" | "media" | "advanced"-The section it lists under.
icon*React.ComponentType<IconProps>-A Koala Icon.
keywordsstring[]-Extra search terms ("todo", "h1").
shortcutstring-The Markdown shortcut shown on the row ("#", "[]").
turnIntoboolean-Offer it in Turn into. Text-like blocks only.
isAvailable(editor: Editor) => boolean-Hide the command when its node is not registered.
isActive(editor: Editor) => boolean-Is the current block already this type (the check in Turn into).
run*(editor: Editor, range?: Range) => void-Runs it. range is the typed /query to delete first; absent from Turn into.

SlashMenuList

The menu's list on its own, for a picker of your own. Forwards <div> props.

PropTypeDefaultDescription
items*BlockCommand[]-The commands to list.
activeIndex*number-The highlighted row.
onActiveIndexChange(index: number) => void-Called when the pointer moves over a row.
onSelect*(command: BlockCommand) => void-Called when a row is clicked.
groupedbooleantrueGroup rows under their labels (labels show only with two or more groups).
emptyLabelstring"No blocks match"Shown when items is empty.
density"compact" | "comfortable"-Row height; falls back to the nearest DensityProvider.
side"top" | "bottom""bottom"Which way it opens, for the enter motion.

blockHandleExtension()

The block keyboard: ⌘⇧↑ and ⌘⇧↓ (Ctrl on Windows) move the current block, or a heading with its whole section when documentEditorHeadings() is on; ⌘D duplicates it.

documentEditorHeadings()

Gives every H1 to H3 a stable slug id (h-launch-plan), so links, an outline and the table of contents can target it, and lets the block keyboard move a heading with its section. readDocumentHeadings(doc) lists them; getSectionRange, moveSection, duplicateSection and deleteSection work on a heading and the blocks under it.

moveBlock, duplicateBlock, deleteBlock, insertBlockBelow

The block operations behind the keyboard, for your own controls: moveBlock(editor, pos, -1 | 1), duplicateBlock(editor, pos), deleteBlock(editor, pos), insertBlockBelow(editor, pos, above?). A move the schema refuses is a no-op.

useDocumentEditor

Returns the live Tiptap Editor from the nearest DocumentEditor, or null on the first client render.

useDocumentEditorStats

Returns { words, characters, readingMinutes } for the live document, updated on every change (reading time at 230 words a minute).

Constants

DOCUMENT_EDITOR_HIGHLIGHTS (the four highlight colours), BLOCK_COMMAND_GROUP_LABELS, and the defaults DEFAULT_DOCUMENT_EDITOR_LABELS and DEFAULT_BUBBLE_MENU_LABELS.

FAQ#

The Rich Text Editor is a field inside a form, with a toolbar. The Document Editor is the page itself: a title, blocks you add with / and move by hand, and a bar over the selection instead of a toolbar.