Customize list numbering and bullets 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> or <ul>. Lists with a matching id export with the corresponding definition; lists without one keep the default ladder for their kind.
A format whose levels use baseStyle: 'bullet' describes a glyph ladder instead of a counter, so the same registry also carries decorative bullets. See Bullet ladders.
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 bulletList and exposes the setOrderedListNumberingFormat(id), toggleOrderedListWithFormat(format?), setBulletListNumberingFormat(id), toggleBulletListWithFormat(format?) and registerNumberingFormats(formats) commands, and tracks the active format of each list kind at the selection via editor.storage.orderedListNumbering. Registered by ConvertKit (opt in via orderedListNumbering: true, or an options object with defaultFormat / defaultBulletFormat / 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 })Bullet lists gain an attribute in stored JSON
Turning this on registers numberingFormat on bulletList as well as orderedList, so
editor.getJSON() now writes attrs: { numberingFormat: null } on a bullet list that names no
format, where it wrote no attrs at all before. This is what an ordered list has always looked
like, and the rendered HTML does not change: a list with no format is still a bare <ul>. Stored
JSON, collaborative documents and snapshot tests that compare a bullet list byte for byte will
see the new key.
Pass an options object instead of true to configure three things:
ConvertKit.configure({
orderedListNumbering: {
// The format id new ordered lists start with. Defaults to `null` (plain `1. 2. 3.`).
defaultFormat: 'outline',
// The same, for new bullet lists.
defaultBulletFormat: 'arrows',
// 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 ordered lists stay clear. |
defaultBulletFormat | null | The same for a newly created bullet list. The two are separate, so neither default lands on the other kind of list. |
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, and also when a list of another kind stands between the selection and that ordered list, because such a list ends the ladder. To ask whether this command will reach a list, use editor.can().setOrderedListNumberingFormat(null) rather than editor.isActive('orderedList'): the two disagree in exactly that case, and only can() matches what the command will do.
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.
Omitting the argument is not the same as passing null. Omit it and the new list takes the configured defaultFormat; pass null and it takes no format at all.
The two commands answer editor.can() about different things, so a toolbar needs both. toggleOrderedListWithFormat answers the same way its plain toggle does, which is true from an ordinary paragraph because it can create the list; disable a "start a list in this format" button on that. setOrderedListNumberingFormat answers whether a list is already there to change, which is false from that same paragraph. Use the setter's answer to pick which command to run, not to disable the button:
// One "apply this format" button, doing the right thing in both places.
if (editor.can().setOrderedListNumberingFormat(null)) {
editor.chain().focus().setOrderedListNumberingFormat(id).run()
} else {
editor.chain().focus().toggleOrderedListWithFormat(id).run()
}The toggle reaches past a list of another kind
A list of another kind ends the ladder for the set command but not for the toggle, which
still sees a non-list position and takes its create branch. With the selection inside a <ul>
nested in an <ol>, toggleOrderedListWithFormat converts that inner <ul> to an <ol> and
then writes the format onto the enclosing <ol>, a list the user was not editing. With a
checklist there instead, it flattens the checklist and lifts its items into the enclosing <ol>,
losing the checkboxes. The toggle's own can() reports true in both positions, so it cannot
warn you, and the branch above reaches the toggle there because the setter reports false. A
toolbar that should leave a checklist alone has to test editor.isActive('taskList') itself.
Bullet lists
Bullet lists have the same pair, targeting the outermost bulletList ancestor:
editor.chain().focus().setBulletListNumberingFormat('arrows').run()
editor.chain().focus().toggleBulletListWithFormat('arrows').run()Everything above holds for the bullet pair, with defaultBulletFormat in place of defaultFormat. setBulletListNumberingFormat returns false in four positions: outside any list, inside an ordered list, inside a checklist, and inside a bullet list reached only by crossing one of those two. A task list is not a bullet list, so it ends the ladder the same way an ordered list does. Branch on editor.can().setBulletListNumberingFormat(null), which is false in all four, the same way the ordered pair does above.
toggleBulletListWithFormat has the same reach as its ordered twin: from inside a checklist it converts the whole checklist into a plain bullet list, losing the checkboxes.
Adding formats after the editor is running
formats is read once, when the editor is created. A registry that only exists later, the usual case being the one a DOCX import reconstructs from the file, goes in with registerNumberingFormats(formats). The command merges by id and refreshes the injected preview stylesheet, so lists already in the document pick up their markers straight away:
editor.commands.registerNumberingFormats(formatsFromTheImport)Merging by id means repeating the call is safe: re-registering an id replaces that entry rather than adding a second one, so importing the same file twice leaves one definition behind. An id you configured in formats is replaced the same way, which lets an imported document override a house format of the same name.
The full registry, your configured formats plus everything registered since, is on the extension's storage. Hand it to the exporter to close the round trip:
ExportDocx.configure({ numberingFormats: editor.storage.orderedListNumbering.numberingFormats })The ImportDocx extension calls registerNumberingFormats for you when orderedListNumbering is on, so an imported document renders its own markers without any wiring. Call it yourself when you load content some other way.
Reading the active format
To reflect the active format in a toolbar, read it from the extension's storage, which stays in sync as the selection and document change:
const activeOrdered = editor.storage.orderedListNumbering.activeNumberingFormat
const activeBullet = editor.storage.orderedListNumbering.activeBulletNumberingFormat
// a format id, or `null` when the selection is not inside a list of that kindEach field answers what its own command would target, so only one is set at a time: inside a bullet list nested in an ordered one, the bullet field reports that list and the ordered field reads null.
NumberingFormatDefinition
interface NumberingFormatDefinition {
id: string
levels: NumberingLevelDefinition[]
}| Field | Type | Description |
|---|---|---|
id | string | Unique within your numberingFormats[]. Serialized into the numberingFormat attribute of the outermost orderedList or bulletList that names it. |
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
suffix?: 'tab' | 'space' | 'nothing'
isLegalNumbering?: boolean
linkedStyle?: string
}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, or the literal 'bullet' for a bullet ladder. Otherwise 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). On a 'bullet' level it is the glyph itself. |
startAt | 1 | Initial counter value. |
alignment | 'left' | Where the marker is justified against numberIndent. See Marker alignment. |
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. A single item can sit somewhere else; see Indents on a single item. |
markerFont | None | Marker-only run formatting (see NumberingMarkerFont below). Survives DOCX export and is reflected by generateNumberingFormatCss in the editor preview. On a 'bullet' level, font is what a Word symbol-font bullet needs. |
suffix | 'tab' | Word's "Follow number with" (w:suff). A tab pads the marker out to textIndent; 'space' draws one non-breaking space after the marker and 'nothing' draws none, so the body text follows the marker directly. Honoured by the editor preview. |
isLegalNumbering | false | Word's "Legal style numbering" (w:isLgl): a %N pointing at a roman or letter level is redrawn in arabic, so Article IV.2 becomes Article 4.2. An arabic level keeps the form it counts in, so decimalZero stays zero-padded (01.1, not 1.1), and a level that counts nothing still draws nothing. Honoured by the editor preview. |
linkedStyle | None | Word's "Link level to style" (w:pStyle): paragraphs in that style number at this level. Names a style id such as 'Heading1'. A format with a linked level is one list for the whole document, so its lists continue each other instead of restarting. Export only; the editor preview has no equivalent. |
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. - A
%Npointing at a level that counts nothing, a'bullet'or'none'level, draws nothing.
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) |
Bullet ladders
Word draws a bullet from the glyph in w:lvlText, so an arrow, diamond or star is something the document carries rather than something the reader's Word decides. A format whose levels use baseStyle: 'bullet' describes that ladder, and a bulletList selects it through the same numberingFormat attribute:
numberingFormats: [
{
id: 'arrows',
levels: [
{ baseStyle: 'bullet', textTemplate: '➔' },
{ baseStyle: 'bullet', textTemplate: '◆' },
],
},
]
// { type: 'bulletList', attrs: { numberingFormat: 'arrows' }, content: [...] }On a 'bullet' level, textTemplate is the glyph itself and markerFont.font names the font to draw it in, which is what a Word symbol-font bullet needs. Everything else on the level works the same way it does for a counter, indents included. Only the exact value bullet marks a level this way; every other baseStyle stays a counter level.
A bullet level counts nothing, so a %N pointing at one draws nothing. 'Item %1:' on a bullet level renders Item :, in Word and in the preview alike. 'none' behaves the same way, and isLegalNumbering does not change it: legal numbering redraws a counter in arabic, but it cannot give a level a counter it does not have.
A format is not tied to a list kind. It applies to whichever list names it, the way a Word numId points at a definition whatever the list looks like, so a glyph ladder on an <ol> draws glyphs and a counter ladder on a <ul> counts. generateNumberingFormatCss selects the attribute on ul and ol alike, so the preview and the export agree either way.
A list nested inside another of the same kind takes the outer list's ladder at its own depth, so one multilevel list keeps one definition.
Task lists
A task list renders as a ul, but it is not a level of a bullet ladder. A checklist keeps its checkboxes, and a bullet list nested inside one keeps its own numberingFormat.
A checklist also ends the chain, exactly as a list of the other kind does. The commands and the active… storage fields stop there, so a bullet list reached only through a checklist is outermost again and carries its own format.
Nesting depth is counted differently on each side
The editor preview counts nesting only through lists of the same kind, while the export counts every enclosing list. A list sitting under a checklist, or under a list of the other kind, therefore draws its format's first level on screen and a deeper level in Word. Keep a multilevel ladder in one kind of list to avoid the difference.
Schema convention: outermost of its own kind
The numberingFormat attribute belongs on the outermost list of each kind. One definition declares all nesting levels for that entire multilevel list. OrderedListNumbering enforces this on parse: pasted HTML with data-numbering-format on an <ol> nested in another <ol>, or on a <ul> nested in another <ul>, has the attribute stripped.
A list of the other kind starts a ladder of its own, and it also ends the chain: a <ul> reached through an <ol> is outermost again, so it keeps a format of its own even when a further <ul> encloses the whole thing. The commands and the active… storage fields stop at that boundary too, so each targets the list the export and the preview actually draw.
All nesting levels under one outermost list 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 |
Marker alignment
alignment says where the marker sits in relation to numberIndent, the position its level gives it. Word justifies the marker against that single point, and the editor preview does the same.
alignment | Where the marker lands |
|---|---|
'left' (default) | Starts at numberIndent and grows to the right. |
'right' | Ends at numberIndent and grows to the left, so markers of different widths line up on their right edge. |
'center' | Straddles numberIndent, half its width on each side. |
Body text sits at textIndent whichever you pick, so a right-aligned marker leaves a wider gap between itself and the text than a left-aligned marker of the same width. Under 'left' and 'center', a marker wider than that gap pushes the body text of its own item further right; a right-aligned marker grows into the space before numberIndent instead and never moves the text. Either way only that one item's text moves: a list nested inside the item still starts at the position its own level gives it.
// Roman numerals get wider as they count up. Right alignment keeps their
// periods in one column and the text in another.
levels: [{ baseStyle: LevelFormat.UPPER_ROMAN, textTemplate: '%1.', alignment: 'right' }]A suffix of 'space' or 'nothing' drops the padded marker column, so under 'left' and 'center' the body text follows the marker directly instead of waiting for textIndent. Combined with 'right' the marker still ends at numberIndent, which leaves the body text starting there as well.
An imported format carries no alignment
The registry a DOCX import hands back does not report the alignment the source document used, so
an imported ladder draws left-aligned even where Word right-aligned it. Set alignment on the
formats you define in code.
Indents on a single item
A level places every item that belongs to it, but a single item can sit somewhere else. When the paragraph inside a list item carries an indent attribute, that item's marker and body text both move to it and the level's textIndent no longer applies to that item. A negative firstLineIndent on the same paragraph sets that item's gap between marker and text, in place of the level's distance from numberIndent to textIndent. A heading inside a list item is read the same way.
// Sits further right than its level puts it, with a 24px gap for the marker.
{
type: 'listItem',
content: [
{
type: 'paragraph',
attrs: { indent: 96, firstLineIndent: -24 },
content: [{ type: 'text', text: 'Positioned by the item, not by the level' }],
},
],
}Imported documents rely on this. The import writes each list paragraph's own Word indent onto the paragraph node as indent and firstLineIndent (see ConvertKit), so the items land where the source document drew them rather than on the level's ladder.
Conditions and limits:
- It applies only to a list that names a format the editor knows about, whether from
formatsor fromregisterNumberingFormats. Without one nothing positions the item, so the paragraph keeps rendering the indent itself aspadding-leftandtext-indent, exactly as a paragraph outside a list does. The same holds whenorderedListNumberingis off. - A task list is not part of a numbering ladder, so an item in a checklist keeps its indent on the paragraph.
- A positive
firstLineIndentis an ordinary first-line indent rather than a marker gap. It stays on the paragraph and indents the first line of the item's text. indentRightis left alone. A list positions the left edge of its items, not the right.- A list item holding more than one block takes its position from the first block. The later blocks line up with it and do not render an indent of their own.
- The attributes stay on the node, so
ExportDocxwrites the same indent back out and a DOCX round trip keeps it. - The editor positions the item and clears the paragraph's own
padding-leftandtext-indentwith an inline style. A rule of your own on that paragraph needs!importantto take the block back.
generateNumberingFormatCss(formats, options?)
Returns the CSS text, and 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. Its rules set the left margin and padding on the list and on each of its items, so a lower-specificity rule of your own on a ul, ol or li inside a formatted list loses to them; vertical spacing is left to you. 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.
List items need box-sizing: border-box. The item rules pair a percentage width with a padding, so without it an item sitting to the right of its level widens past the list's right edge. ConvertKit's CSS reset sets it inside .tiptap, which covers the default scope; add it yourself if you inject this CSS anywhere else.
Every format emits both ul[data-numbering-format="…"] and ol[data-numbering-format="…"] rules, because a format applies to whichever list names it. Each chain then steps to the next depth with > li > through its own element (ul:not([data-type="taskList"]) or ol), so a checklist keeps its checkboxes, and a ladder stops at a list of the other kind rather than reaching through it.
Resolution rules
- A matched id renders every level of that multilevel list (including all nested lists of the same kind) using the corresponding definition.
- An unmatched id (typo, missing attribute, empty
numberingFormats) keeps the default ladder for that list kind: plain1. 2. 3.for an ordered list, the built-in glyph ladder for a bullet list. - Sibling multilevel lists referencing the same format restart independently, each starting at its own
startAt. A format with alinkedStyleon any level is the exception: it is one list for the whole document, so its lists continue each other.
Known limitations
Word features the exporter intentionally does not model:
| Feature | Workaround / scope |
|---|---|
Per-level counter-restart (Word's "restart list after level N", w:lvlRestart) | Word's default behavior (sub-levels restart when the parent advances) is always used. |
Picture bullets (w:lvlPicBulletId) | Use a symbol font through markerFont.font instead. |
| Continuing numbering across separate lists | Each list starts at its startAt independently. |
| Per-item marker overrides (a different glyph or counter value on one item) | Use a separate list to start at a different counter value. Where an item sits is a separate matter; see Indents on a single item. |
| DOCX → editor import | Handled by @tiptap-pro/extension-import-docx, which feeds the formats it reconstructs to the editor. See Adding formats after the editor is running. |
See also
- Editor extension overview: base
ExportDocxconfiguration. - Styles:
styleOverridesfor paragraph styles, headings, list-paragraph styles. - REST API: server-side conversion endpoint.