Customize ordered list numbering for DOCX export
@tiptap-pro/extension-export-docx accepts an optional registry of numbering format definitions — multilevel marker style, marker text, alignment, indent, and font — selected per-list via an attribute on the outermost <ol>. Lists with a matching id export with the corresponding definition; lists without one export as plain 1. 2. 3..
The feature is opt-in. Consumers who don't supply numberingFormats see no behavior change.
What's provided
| Export | Package | Purpose |
|---|---|---|
OrderedListNumbering | @tiptap-pro/extension-convert-kit | Tiptap extension that adds the numberingFormat attribute to orderedList and exposes the setOrderedListNumberingFormat(id) and toggleOrderedListWithFormat(format?) commands, and tracks the active format at the selection via editor.storage.orderedListNumbering. Registered by ConvertKit (opt in via orderedListNumbering: true, or an options object with defaultFormat / formats). Enforces outermost-only numbering on paste, JSON, collaborative sync, and programmatic edits. |
generateNumberingFormatCss(formats, options?) | @tiptap-pro/extension-convert-kit | Pure, dependency-free function that returns CSS text for the editor preview — same registry, same visual result. |
NumberingFormatDefinition, NumberingLevelDefinition, NumberingMarkerFont | @tiptap-pro/extension-convert-kit | The data shape your registry follows. Structurally compatible with ExportDocx's numberingFormats config. |
ExportDocx.configure({ numberingFormats }) | @tiptap-pro/extension-export-docx | Pass the registry to the exporter so the .docx carries the matching definitions. |
LevelFormat, IRunOptions, PositiveUniversalMeasure | @tiptap-pro/extension-export-docx | Re-exported from docx so you don't add a second dependency. |
A picker UI for end users isn't part of these packages — that's an application concern and depends on your component library.
Quick start
import { Editor } from '@tiptap/core'
import { ConvertKit, type NumberingFormatDefinition } from '@tiptap-pro/extension-convert-kit'
import { ExportDocx, LevelFormat } from '@tiptap-pro/extension-export-docx'
// 1. Define your registry — one source of truth, used on both sides below.
const MY_FORMATS: NumberingFormatDefinition[] = [
{
id: 'decimal-paren',
levels: [
{ baseStyle: LevelFormat.DECIMAL, textTemplate: '%1)' },
{ baseStyle: LevelFormat.LOWER_LETTER, textTemplate: '%2)' },
{ baseStyle: LevelFormat.LOWER_ROMAN, textTemplate: '%3)' },
],
},
{
id: 'outline',
levels: [
{ baseStyle: LevelFormat.DECIMAL, textTemplate: '%1.' },
{ baseStyle: LevelFormat.DECIMAL, textTemplate: '%1.%2.' },
{ baseStyle: LevelFormat.DECIMAL, textTemplate: '%1.%2.%3.' },
],
},
]
// 2. Opt in via ConvertKit. Passing `formats` generates the editor-preview CSS
// and injects it for you; register ExportDocx with the same registry so the
// .docx carries the matching definitions.
const editor = new Editor({
extensions: [
ConvertKit.configure({
// Off by default so consumers without custom numbering keep a clean
// orderedList schema. Pass an options object (or `true`) to opt in.
orderedListNumbering: { formats: MY_FORMATS },
}),
ExportDocx.configure({
numberingFormats: MY_FORMATS,
onCompleteExport: (blob) => {
/* download blob */
},
}),
],
})
// 3. Start a numbered list in the chosen format at the current selection
// (or, when already in a list, change its format with setOrderedListNumberingFormat).
editor.chain().focus().toggleOrderedListWithFormat('outline').run()Prefer to manage the stylesheet yourself? Omit formats and inject the CSS from generateNumberingFormatCss instead — the two paths produce the same markers.
Enabling ordered list numbering
OrderedListNumbering ships with @tiptap-pro/extension-convert-kit but is off by default, to keep the orderedList schema clean for consumers that don't use custom numbering. Opt in via ConvertKit:
ConvertKit.configure({ orderedListNumbering: true })Pass an options object instead of true to configure two things:
ConvertKit.configure({
orderedListNumbering: {
// The format id new ordered lists start with. Defaults to `null` (plain `1. 2. 3.`).
defaultFormat: 'outline',
// The same definitions you pass to ExportDocx. When provided, the
// editor-preview CSS is generated from them and injected automatically,
// so on-screen markers match the export with no extra wiring.
formats: MY_FORMATS,
},
})| Option | Default | Description |
|---|---|---|
defaultFormat | null | The numbering format id applied to a newly created ordered list (the numberingFormat attribute default). Only the outermost list is formatted; nested lists stay clear. |
formats | null | Numbering format definitions used to generate and inject the editor-preview CSS — the same array passed to ExportDocx.configure({ numberingFormats }). Editors configured with the same formats share one preview stylesheet, while editors with different formats get separate ones, so each always shows the correct markers; a shared stylesheet is removed only once the last editor using it is destroyed. |
Passing formats here is the equivalent of calling generateNumberingFormatCss and injecting the result yourself — choose whichever fits your setup.
Applying a format
The setOrderedListNumberingFormat(id) command sets the numberingFormat attribute on the outermost ordered list ancestor of the current selection. Pass null to clear it (the list then exports as plain 1. 2. 3.).
editor.chain().focus().setOrderedListNumberingFormat('decimal-paren').run()
editor.chain().focus().setOrderedListNumberingFormat(null).run()The command returns false when the selection isn't inside an ordered list.
Starting a new list in a format
setOrderedListNumberingFormat only targets a list the selection is already inside. To start a numbered list in a chosen format from a non-list selection, use toggleOrderedListWithFormat(format?) — it creates the ordered list and applies the format in one call, which is the natural action for a "pick a format to start a numbered list" toolbar button:
// From a paragraph: create an ordered list and set its format.
editor.chain().focus().toggleOrderedListWithFormat('outline').run()
// Omit the format to start a list in the configured `defaultFormat`.
editor.chain().focus().toggleOrderedListWithFormat().run()When the selection is already inside an ordered list, the command toggles the list off, mirroring toggleOrderedList. Chaining toggleOrderedList().setOrderedListNumberingFormat(format) reaches the same end state; the single command is simply more convenient and also handles the toggle-off case.
To reflect the active format in a toolbar, read it from the extension's storage — it stays in sync as the selection and document change:
const activeFormatId = editor.storage.orderedListNumbering.activeNumberingFormat
// a format id, or `null` when the selection is not inside an ordered listNumberingFormatDefinition
interface NumberingFormatDefinition {
id: string
levels: NumberingLevelDefinition[]
}| Field | Type | Description |
|---|---|---|
id | string | Unique within your numberingFormats[]. Serialized into the orderedList's numberingFormat attribute. |
levels | NumberingLevelDefinition[] | One entry per nesting depth. Must be non-empty. When a list nests deeper than the array, depth N reuses levels[N % levels.length]. |
NumberingLevelDefinition
interface NumberingLevelDefinition {
baseStyle: NumberingBaseStyle
textTemplate: string
startAt?: number
alignment?: 'left' | 'center' | 'right'
numberIndent?: number | string
textIndent?: number | string
markerFont?: NumberingMarkerFont
}ConvertKit's types use plain string literals so the package does not pull in docx. The fields are structurally compatible with ExportDocx's stricter (docx-typed) version, so passing LevelFormat.DECIMAL from @tiptap-pro/extension-export-docx works the same as passing the string 'decimal'.
| Field | Default | Description |
|---|---|---|
baseStyle | (required) | The counter glyph, equivalent to the matching docx LevelFormat value. The Latin/numeric styles ('decimal', 'decimalZero', 'upperLetter', 'lowerLetter', 'upperRoman', 'lowerRoman', 'none') and many locale styles (e.g. 'hebrew1', 'thaiNumbers', 'hindiNumbers', 'japaneseCounting', 'koreanDigital', 'chineseCounting', 'ideographDigital') map to a matching CSS counter style so the preview renders their native glyph. Styles with no faithful CSS equivalent (e.g. 'chicago', 'ordinal', 'cardinalText') — and any unrecognized string — fall back to decimal in the editor preview only; the exported .docx always carries the exact LevelFormat you set. |
textTemplate | (required) | Marker text using Word's <w:lvlText> grammar — see below. |
startAt | 1 | Initial counter value. |
alignment | 'left' | Marker text alignment within the number area. |
numberIndent | Word's per-level default | Distance from the page margin to the marker. Twips number or a docx-style measure string ('0.63cm', '0.25in', '18pt'). |
textIndent | Word's per-level default | Distance from the page margin to the body text. Should be greater than numberIndent. |
markerFont | — | Marker-only run formatting (see NumberingMarkerFont below). Survives DOCX export and is reflected by generateNumberingFormatCss in the editor preview. |
NumberingMarkerFont
interface NumberingMarkerFont {
font?: string | { name: string }
size?: number | string
bold?: boolean
italics?: boolean
color?: string
underline?: unknown
}| Field | Description |
|---|---|
font | Font family name, or a docx-style { name } object. |
size | docx half-points as a number (so 28 = 14pt), or a docx measure string such as '14pt'. |
bold | Set true to render the marker bold. |
italics | Set true to render the marker italic. |
color | Hex color, with or without leading #. |
underline | Any truthy value applies text-decoration: underline in the preview. |
Mirrors the subset of docx IRunOptions that generateNumberingFormatCss understands. Additional fields you pass through to ExportDocx are ignored by the preview but still survive into the .docx.
The textTemplate grammar
%1through%9reference the counter at the 1-indexed nesting depth. So%1is always "the counter at the outermost level", regardless of which level thetextTemplatebelongs to.- All other characters render literally.
- Stray
%followed by a non-digit (e.g."50%") is preserved.
To make each level display its own counter, use ascending %N per level. To make a level include parent counters (legal-style outlines), chain them: '%1.%2.' at level 1 renders '1.1.'.
| Template at level N | Rendered example |
|---|---|
'%1.' at level 0 | 1. |
'%2.' at level 1 | 1. (the level-1 counter) |
'%1.%2.' at level 1 | 1.1. |
'%1.%2.%3.' at level 2 | 1.1.1. |
'Article %1' at level 0 | Article 1 |
'§ %1 —' at level 0 | § 1 — |
'(%3)' at level 2 | (1) |
Schema convention — outermost only
The numberingFormat attribute belongs on the outermost <ol> only. One definition declares all nesting levels for the entire multilevel list. OrderedListNumbering enforces this on parse — pasted HTML with data-numbering-format on a nested <ol> has the attribute stripped.
All nesting levels under one outermost <ol> share a single counter scope, and sub-levels restart when the parent advances, exactly as Word does.
Cycling rule
levels.length defines the cycle period. When a list nests deeper than levels.length - 1, depth N reuses levels[N % levels.length] for both DOCX export and the editor preview CSS. Supply nine levels to cover Word's full nesting depth without cycling; supply fewer when the deeper levels can naturally repeat the shallower entries.
Defaults match Word's standard
When numberIndent / textIndent are omitted, the exporter and the CSS helper both emit Word's standard multilevel-list defaults:
| Depth | textIndent | numberIndent | Hanging |
|---|---|---|---|
| 0 | 720 (0.50″) | 360 (0.25″) | 360 |
| 1 | 1140 (0.79″) | 780 (0.54″) | 360 |
| 2 | 1440 (1.00″) | 1080 (0.75″) | 360 |
| 3 | 1740 (1.21″) | 1380 (0.96″) | 360 |
| 4 | 2040 (1.42″) | 1680 (1.17″) | 360 |
| 5 | 2340 (1.63″) | 1980 (1.38″) | 360 |
| 6 | 2640 (1.83″) | 2280 (1.58″) | 360 |
| 7 | 2940 (2.04″) | 2580 (1.79″) | 360 |
| 8 | 3240 (2.25″) | 2880 (2.00″) | 360 |
generateNumberingFormatCss(formats, options?)
Returns the CSS text — you choose how to inject it (a <style> tag, a CSS-in-JS layer, a stylesheet served by your build pipeline, etc.). The function is pure and dependency-free, safe to call at boot or whenever your registry changes.
generateNumberingFormatCss(formats, {
scope: '.tiptap.ProseMirror', // default
maxDepth: 9, // default
})| Option | Default | Description |
|---|---|---|
scope | '.tiptap.ProseMirror' | CSS selector prefix scoping every emitted rule. Pass an empty string for unscoped output (handy if you inject inside Shadow DOM or a CSS layer of your own). |
maxDepth | 9 | Maximum nesting depth to emit rules for. Lower values produce smaller stylesheets. |
The generated CSS positions each list marker and its body text to match the absolute positions Word renders, including the stair-step indents at every nesting depth. If you need to override anything for theming (dark mode, RTL, font scaling), wrap the output in your own selector or scope and rely on the cascade.
Resolution rules
- A matched id renders every level of that multilevel list — including all nested
<ol>s — using the corresponding definition. - An unmatched id (typo, missing attribute, empty
numberingFormats) renders as plain1. 2. 3.numbering. - Sibling multilevel lists referencing the same format restart independently — each starts at its own
startAt.
Known limitations
Word features the exporter intentionally does not model:
| Feature | Workaround / scope |
|---|---|
| Forcing parent-counter refs to render as arabic regardless of base style (Word's "legal numbering" override) | Define a separate format with all-DECIMAL levels for the same visual output. |
| Per-level counter-restart customization | Word's default behavior (sub-levels restart when the parent advances) is always used. |
| Marker-text suffix (tab / space / nothing between marker and body) | Always tab (Word's default). |
| Continuing numbering across separate lists | Each list starts at its startAt independently. |
| Per-item marker overrides | Use a separate list to start at a different counter value. |
| DOCX → editor import | Handled separately by @tiptap-pro/extension-import-docx. |
See also
- Editor extension overview — base
ExportDocxconfiguration. - Styles —
styleOverridesfor paragraph styles, headings, list-paragraph styles. - REST API — server-side conversion endpoint.