---
title: "Customize list numbering and bullets for DOCX export"
description: "Define multilevel numbering formats and decorative bullet ladders that survive DOCX export, covering base style, marker template, alignment, indent, and font."
canonical_url: "https://tiptap-docs.lin52025i.qzz.io/conversion/export/docx/ordered-list-numbering"
---

# Customize list numbering and bullets for DOCX export

Define multilevel numbering formats and decorative bullet ladders that survive DOCX export, covering base style, marker template, alignment, indent, and font.

- **1. Activate trial or subscribe**

  Start a [free trial](https://cloud.tiptap.dev/v2?trial=true) or [subscribe to the Start
  plan](https://cloud.tiptap.dev/v2/billing) in your account.
- **2. Install from private registry**

  To install this frontend extension, authenticate to Tiptap's private npm registry by following
  the [setup guide](https://tiptap-docs.lin52025i.qzz.io/guides/pro-extensions.md).

`@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](#bullet-ladders).

The feature is opt-in. Consumers who don't supply `numberingFormats` see no behavior change.

> **Interactive demo:** [ExportDocxOrderedListNumbering](https://embed-pro.tiptap.dev/preview/Extensions/ExportDocxOrderedListNumbering)

## 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` | Preview types. The exporter provides stricter versions that work with both packages.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                |
| `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

```ts
import { Editor } from '@tiptap/core'
import { ConvertKit } from '@tiptap-pro/extension-convert-kit'
import {
  ExportDocx,
  LevelFormat,
  type NumberingFormatDefinition,
} 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`](#generatenumberingformatcssformats-options) instead. The two paths produce the same markers.

## Enabling ordered list numbering

`OrderedListNumbering` ships with [`@tiptap-pro/extension-convert-kit`](https://tiptap-docs.lin52025i.qzz.io/conversion/import/docx/convertkit.md) but is **off by default**, to keep the `orderedList` schema clean for consumers that don't use custom numbering. Opt in via `ConvertKit`:

```ts
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:

```ts
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`](#generatenumberingformatcssformats-options) 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.`).

```ts
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:

```ts
// 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:

```ts
// 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:

```ts
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:

```ts
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:

```ts
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:

```ts
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 kind
```

Each 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`

```ts
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`

```ts
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 accept plain strings so the package does not pull in `docx`. For a registry shared by ConvertKit and `ExportDocx`, import `NumberingFormatDefinition` from `@tiptap-pro/extension-export-docx`. Its stricter types work with both packages. `LevelFormat.DECIMAL` and the string `'decimal'` both select decimal numbering.

| Field              | Default                  | Description                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                  |
| ------------------ | ------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `baseStyle`        | (required)               | The counter glyph, or the literal `'bullet'` for a [bullet ladder](#bullet-ladders). Otherwise equivalent to the matching docx [`LevelFormat`](https://docx.js.org/api/enums/LevelFormat.html) 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](#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](#indents-on-a-single-item).                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                      |
| `markerFont`       | None                     | Marker-only run formatting (see [`NumberingMarkerFont`](#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`

```ts
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.                     |

`generateNumberingFormatCss` reads the fields above. Use the exporter's `NumberingMarkerFont` for marker fonts shared with `ExportDocx`. It also accepts other run options that only affect the exported document.

```ts
import { Docx, type NumberingMarkerFont } from '@tiptap-pro/extension-export-docx'

const markerFont: NumberingMarkerFont = {
  color: 'AA3344',
  underline: { type: Docx.UnderlineType.SINGLE, color: '336699' },
}
```

Both `color` and `underline.color` use strings. Native [`IRunOptions`](https://docx.js.org/api/interfaces/IRunOptions.html) also accepts theme-color objects such as `{ theme: 'accent1' }`, so use `NumberingMarkerFont` rather than `IRunOptions` to annotate shared marker fonts.

## The `textTemplate` grammar

- `%1` through `%9` reference the counter at the 1-indexed nesting depth. So `%1` is always "the counter at the outermost level", regardless of which level the `textTemplate` belongs to.
- All other characters render literally.
- Stray `%` followed by a non-digit (e.g. `"50%"`) is preserved.
- A `%N` pointing 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:

```ts
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.

```ts
// 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.

```ts
// 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](https://tiptap-docs.lin52025i.qzz.io/conversion/import/docx/convertkit.md)), 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 `formats` or from [`registerNumberingFormats`](#adding-formats-after-the-editor-is-running). Without one nothing positions the item, so the paragraph keeps rendering the indent itself as `padding-left` and `text-indent`, exactly as a paragraph outside a list does. The same holds when `orderedListNumbering` is 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** `firstLineIndent` is 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.
- `indentRight` is 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 `ExportDocx` writes the same indent back out and a DOCX round trip keeps it.
- The editor positions the item and clears the paragraph's own `padding-left` and `text-indent` with an inline style. A rule of your own on that paragraph needs `!important` to 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.

```ts
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: plain `1. 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 a `linkedStyle` on 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](#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](#adding-formats-after-the-editor-is-running). |

## See also

- [Editor extension overview](https://tiptap-docs.lin52025i.qzz.io/conversion/export/docx/editor-extension.md): base `ExportDocx` configuration.
- [Styles](https://tiptap-docs.lin52025i.qzz.io/conversion/export/docx/styles.md): `styleOverrides` for paragraph styles, headings, list-paragraph styles.
- [REST API](https://tiptap-docs.lin52025i.qzz.io/conversion/export/docx/rest-api.md): server-side conversion endpoint.
