Skip to content

Tiptap Rich Text Editor

This document describes our custom Tiptap-based WYSIWYG editor, its modules, toolbar system, and the HTML transforms we built to work around Tiptap's limitations.

Architecture Overview

The editor is built around three core pieces:

  • TiptapEditor — the main class that wires everything together: initializes the Tiptap instance, resolves which modules to load based on the wysiwyg profile, manages the toolbar and context menu, and handles HTML transforms on input/output.
  • Toolbar — renders toolbar buttons grouped by category and keeps their active state in sync with the editor selection.
  • ContextMenu — a right-click menu that shows context-sensitive actions (e.g. table operations when clicking inside a table).

Every feature is encapsulated in a module. A module can provide extensions, toolbar buttons, context menu items, and HTML transforms. The editor collects all modules, filters them based on the active profile, and registers only the relevant extensions and UI elements.

Modules & Features

Basic Styles

Bold, italic, and strikethrough are always available. Subscript and superscript are opt-in and only load when the profile includes the basicstyles extra plugin.

History (Undo / Redo)

Provides undo and redo buttons backed by Tiptap's built-in UndoRedo extension.

Lists

Supports ordered (numbered) and unordered (bulleted) lists. Each list type registers its own extension and toolbar button.

Indentation

Handles indentation for paragraphs and headings via a custom indent global attribute that renders as margin-left. When the cursor is inside a list, the indent/outdent buttons sink or lift list items instead.

Text Alignment

Left, center, right, and justify alignment using Tiptap's TextAlign extension configured for headings and paragraphs. The "left" button works by unsetting alignment (since left is the default).

Insert

Two separate sub-modules:

  • Horizontal Rule — inserts an <hr> element.
  • Blockquote — toggles a block quote with active state tracking.

Cleanup

A single "Remove Format" button that strips all marks from the selection.

Tables

The most complex module. It provides a full table editing experience with insert/edit dialogs, cell property dialogs, a context menu with row/column/cell operations, and several HTML transforms. See the dedicated section below for details.

Table Support in Detail

Custom Extensions

We extend several default Tiptap table nodes to support extra attributes:

  • CustomTable — adds class, id, summary, align, border, cellpadding, cellspacing, width, height, and a dataUserStyle attribute that preserves inline styles through editing.
  • CustomTableCell — adds the same dataUserStyle attribute to cells.
  • CustomTableHeader — extends the default TableHeader with a scope attribute (col, row, or null) for accessibility.
  • TableFigure and TableCaption — two custom nodes for wrapping tables with captions (see below).

Table Dialog

The table properties dialog allows you to configure headers, dimensions, alignment, border, padding, spacing, CSS class, ID, inline style, caption, and summary — both when inserting a new table and when editing an existing one.

Cell Dialog

The cell properties dialog lets you change cell type (data vs header), column/row span, width, height, word wrap, and horizontal/vertical alignment.

Context Menu

Right-clicking inside a table opens a context menu with grouped sub-menus:

  • Cell — insert cell before/after, clear cells, merge, split, cell properties.
  • Row — insert row before/after, delete rows.
  • Column — insert column before/after, delete columns.
  • Table-level — delete table, table properties.

All cell/row/column actions are disabled when the cursor is inside a caption.

HTML Transforms

Tiptap's internal document model does not natively support <caption>, <thead>/<tbody>, or <figure> wrappers around tables. We solve this with HTML transforms — functions that run on the HTML string both when content enters the editor and when it leaves.

Each transform has a toEditor and a toOutput method. They operate on a parsed DOM so we can restructure elements before Tiptap sees them (and restore the original structure on output).

Table Caption Transform

Problem: Tiptap has no concept of a <caption> element inside a <table>. Any <caption> in the source HTML would be dropped.

Solution: On input (toEditor), we find every <table> that contains a <caption>, wrap the table in a <figure data-type="table">, convert the <caption> into a <figcaption>, and append it after the table inside the figure. Two custom Tiptap nodes — TableFigure and TableCaption — handle this structure in the editor.

On output (toOutput), the transform reverses the process: it unwraps the figure, converts the <figcaption> back into a <caption>, and places it inside the table. The result is clean, standard HTML.

Table Header / Thead Transform

Problem: Tiptap flattens all table rows into a single level. It does not distinguish between <thead>, <tbody>, and <tfoot> sections. If the source HTML contains these wrappers, rows may be duplicated or lost.

Solution: On input, the transform moves all rows from <thead>, <tbody>, and <tfoot> directly under the <table> element, then removes the now-empty section wrappers. Tiptap sees a flat list of <tr> elements and parses them normally.

On output, the transform inspects the first row: if all its cells are <th> elements, that row goes into a new <thead> and the rest go into a <tbody>. If there are no header cells, everything goes into a single <tbody>. This restores semantically correct HTML.

Table Cleanup Transform

Problem: Various table attributes and styles need special handling to survive the round-trip through Tiptap without loss or duplication.

Solution: On input, colgroup elements are removed (Tiptap doesn't use them), redundant colspan="1" and rowspan="1" attributes are stripped, and inline style attributes on tables and cells are moved to a data-user-style attribute so Tiptap doesn't interfere with them.

On output, the data-user-style values are merged back into the style attribute, width and height table attributes are folded into the style string, colgroup leftovers are cleaned up again, and min-width values injected by the table editing UI are stripped from cell styles.

Transform Pipeline

All three transforms run in sequence through a shared pipeline in TiptapEditor.transformHtml(). The method parses the HTML into a DOM, calls every registered transform in order, and serializes the result back to an HTML string. This keeps the transform logic decoupled from the editor core — modules simply declare their transforms and the editor handles the rest.

Profile-Based Configuration

The editor adapts to different use cases through a WysiwygProfile. The profile controls:

  • Toolbar groups — which groups of buttons appear, and in what order.
  • Removed buttons — individual buttons that should be hidden.
  • Extra plugins — opt-in features like subscript/superscript.
  • Default table class — a CSS class automatically applied to new tables.

Modules can implement an isEnabled check that reads the profile to decide whether they should be loaded at all.