Repository navigation
(Question) Change numbering style of nested ordered lists #587
Description
Activity
I'm just a user of this library, but implementing this to the core list block would make lists much more complex than they need to be for most users (I can only speak for myself). Writing a custom block using the core Blocknote ListBlock as a basis would be what I would do in the short term.
You can also just use the slash menu in any nested block too:
Just to offer some alternatives for you in the meantime.
I think this is a good suggestion! While customizing the numbering style for each nesting level is probably overkill, I agree that nested numbered lists should have distinct numbering styles.
Reacted by Stephen BusseyHaving this in the base
NumberedListItemBlockseems very useful. Unless I'm missing something, I don't think it's super easy to implement this even in a custom block today. If there's a path towards implementing it, I'd love to know so I can take a stab at it.Yeah agreed it's not a super easy change, but definitely take a look at the
NumberedListIndexingPlugin. I think in theory you would just need to check the depth fromblockInfoand have a function which converts the regular index to/from the other formats, like roman numerals. Also had a quick look and the TipTap node doesn't care what theindexattribute is since it just stores, renders, and parses it, so doing e.g.index: "iii"shouldn't cause any issues.@matthewlipski I know a lot more about BlockNote now, so was more comfortable taking this on.
I'm not sure how to actually work in the dev version of the project. I was hoping to be able to write some tests or examples to test this feature, but I think they're based on published version of blocknote and not local.
I'm more than happy to work through putting this into BlockNote proper, or if you want to use the code that is also good with me.
A few notes on the code:
- I wasn't sure how to best pass the function into the plugin. Because I am overriding the plugin itself, I decided to put it into the closure. I don't think this would work in the main project.
- The plugin actually becomes simpler because we calculate the list index. This means that the requirement of firstInDoc + firstOfType is not really needed.
- getListCharacter could be default-included in this project. I kept it dependency free
- I could not use the existing index attribute to calculate "previous index", because it's no longer guaranteed to be a digit. I did not want to extend the existing types at all as part of this, and think the re-calculation of depth/index is probably good overall.
- BUG: I realized that
depthis not calculate properly here. It should be the depth of the list entry and NOT the depth relative to the document. I think that calculation would need to change to be accurate. Impact: a numeric list started at depth will not necessarily start at 1
// NumberListIndexingPlugin.ts import { Plugin, PluginKey } from "prosemirror-state" import { getBlockInfoFromPos } from "../helpers/getBlockInfoFromPos" // ProseMirror Plugin which automatically assigns indices to ordered list items per nesting level. const PLUGIN_KEY = new PluginKey(`numbered-list-indexing`) interface Options { getListCharacter?: (positionDetails: { depth: number; index: number }) => string } const defaultGetListCharacter = (position: { index: number }) => position.index.toString() export const NumberedListIndexingPlugin = (opts: Options = {}) => { const getListCharacter = opts.getListCharacter || defaultGetListCharacter return new Plugin({ key: PLUGIN_KEY, appendTransaction: (_transactions, _oldState, newState) => { const tr = newState.tr tr.setMeta("numberedListIndexing", true) let modified = false // Traverses each node the doc using DFS, so blocks which are on the same nesting level will be traversed in the // same order they appear. This means the index of each list item block can be calculated by incrementing the // index of the previous list item block. newState.doc.descendants((node, pos) => { if (node.type.name === "blockContainer" && node.firstChild!.type.name === "numberedListItem") { const blockInfo = getBlockInfoFromPos(tr.doc, pos + 1)! if (blockInfo === undefined) { return } // Divide by 2 because each block has a nesting around it const depth = blockInfo.depth / 2 let firstListBlock = blockInfo let blockIndex = 1 while (firstListBlock) { const prevBlockInfo = getBlockInfoFromPos(tr.doc, firstListBlock.startPos - 2)! if ( prevBlockInfo && prevBlockInfo.id !== firstListBlock.id && prevBlockInfo.depth === firstListBlock.depth && prevBlockInfo.contentType === firstListBlock.contentType ) { blockIndex++ firstListBlock = prevBlockInfo } else { break } } const newIndex = getListCharacter({ depth, index: blockIndex }) const contentNode = blockInfo.contentNode const index = contentNode.attrs["index"] if (index !== newIndex) { modified = true tr.setNodeMarkup(pos + 1, undefined, { index: newIndex }) } } }) return modified ? tr : null } }) }
// getListCharacter.ts export function getListCharacter(positionDetails: { depth: number; index: number }) { const style = (positionDetails.depth - 1) % 3 if (style === 1) { return getColumnLetters(positionDetails.index) } else if (style === 2) { return romanize(positionDetails.index).toLowerCase() } else { return `${positionDetails.index}` } } // from https://dev.to/all_stacks_developer/how-to-convert-column-index-of-a-spreadsheet-into-letters-31k0 // prettier-ignore const ALPHABET = ['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z'] function getColumnLetters(columnIndexStartFromOne: number): string { if (columnIndexStartFromOne < 27) { return ALPHABET[columnIndexStartFromOne - 1] } else { var res = columnIndexStartFromOne % 26 var div = Math.floor(columnIndexStartFromOne / 26) if (res === 0) { div = div - 1 res = 26 } return getColumnLetters(div) + ALPHABET[res - 1] } } // from https://stackoverflow.com/questions/9083037/convert-a-number-into-a-roman-numeral-in-javascript function romanize(num: number): string { const digits = `${num}`.split("") // prettier-ignore const key = ["","C","CC","CCC","CD","D","DC","DCC","DCCC","CM", "","X","XX","XXX","XL","L","LX","LXX","LXXX","XC", "","I","II","III","IV","V","VI","VII","VIII","IX"] let roman = "" let i = 3 while (i--) roman = (key[+digits.pop()! + i * 10] || "") + roman return Array(+digits.join("") + 1).join("M") + roman }
// editor.tsx defaultBlockSpecs.numberedListItem.implementation.node.config.addProseMirrorPlugins = () => { return [ NumberedListIndexingPlugin({ getListCharacter }) ] }
This updated version handles broken lists properly, by finding the closest parent of the same type: https://gist.github.com/sb8244/c59acf0836eda8c79852e623afe936d1
Just solved the same problem. I'm using CSS to generate the actual ordering symbols (
lower-alphain this case):@property --alpha-index { syntax: "<integer>"; initial-value: 1; inherits: true; } .bn-block-content[data-content-type="alphabeticalListItem"] { display: flex; gap: 0.5em; } .bn-block-content[data-content-type="alphabeticalListItem"]::before { counter-reset: alphabeticalCounter var(--alpha-index); content: counter(alphabeticalCounter, lower-alpha) "."; }
And extended my ListIndexingPlugin like so:
tr.setNodeMarkup(blockInfo.blockContent.beforePos, undefined, { ...contentNode.attrs, index: newIndex, style: `--alpha-index: ${newIndex}` })
For my
AlphabeticalListItem(based onNumericalListItem) to work I had to patchcoreto exportupdateblock, though.export * from "./api/blockManipulation/commands/updateBlock/updateBlock.js";
Happy to learn about a workaround – or have that exported by default...? 🙂
Reacted by Alex3236 and Tempura- addedneeds-triageIssue has not yet been reviewed or classified by maintainers.Issue has not yet been reviewed or classified by maintainers.
on Feb 3, 2026 - locked and limited conversation to collaborators
on Feb 19, 2026

At #62, @brandondrew raised the issue of incorrect numbering.
#42 fixed the numbering, but it doesn't solve the other problem: sublists commonly have a different number style.
For example, a common numbered list shall look like this, which is more readable:
while current presentation is:
common nested styles are:
Further more, there are multiple variants of one number style, which is useful in different scenarios. For example,
a.could also bea)or(a).If possible, consider implementing this, or provide a way to customize this behavior easily. Maybe do something like this: