ProseMirror Flat list (Alpha)

Hello everyone, I’m excited to share ProseMirror Flat List, an alternative to the official prosemirror-schema-list. My aim is to provide a top-notch and native feeling list that is equal to or even better than what Google Docs and Notion have currently.

You can read more about the difference between this project and prosemirror-schema-list in the project README.

This project is still in the alpha stage. Any comments are appreciated!


Very cool!

One thing that might cause issues for people is the use of Tab and Shift-Tab—keyboard-only users need those to move focus in the page, so it is recommended to not override them for custom functionality.

1 Like

Do you have plans to support listType as well ? Basically when we go nested based on the level the type will change


1 Like

Cool! I also spent a considerable amount of time fixing a list implementation that used flat list items (like this czi-prosemirror demo). Don’t know if you update the ordered list numbers but that was a doozy to build. I still kept the original <ul> and <ol> tags and just changed <li> indent property when tabbed. Don’t think I allowed nesting block nodes inside <li> elements though. I also included changing of list-style based on indent depth.

I could make a PR if you want to take a look and decide what to incorporate if anything. Surprisingly difficult, I must say. TipTap seemed to have a pretty good version as well. You should also make some copy-paste tests for at least Docs, Word, Notion and Apple Notes. Apple Notes uses some annoyingly weird schema.


Really cool project. I think it would be better if you can let the users pass the group attribute in spec.

1 Like

Can you please share the source for list style based on depth ? I assume you override the tiptap functionality ?

1 Like

I didn’t use TipTap, it was just vanilla ProseMirror. Here’s ordered list

export const ordered_list: NodeSpec = {
  attrs: {
    indent: { default: 0 },
    start: { default: 1 },
  content: 'list_item+',
  group: 'block',
  parseDOM: [
      tag: 'ol',
      getAttrs: dom => {
        if (dom instanceof HTMLElement) {
          return {
            indent: parseInt(dom.getAttribute('data-indent') || '0'),
            start: parseInt(dom.getAttribute('start') || '1'),
        return null
  toDOM: (node: PMNode) => {
    const { indent, start } = node.attrs
    const listStyle =
      indent % 3 === 0 ? 'decimal' : indent % 3 === 1 ? 'lower-alpha' : 'lower-roman'
    const attrs = {
      'data-indent': indent,
      style: `list-style: ${listStyle};`,
    return ['ol', attrs, 0]

1 Like

Thanks for sharing, how does the value of data-indent gets updated ? I tried to replicate the same indent extension from czi repo, but when I hit tab on a list item my cursor is automatically navigating to the next line.

Np! As I said, there’s some insane logic to keep that synchronized so :sweat_smile: Basically you just check all the lists that were changed and then update their indents & start counters as necessary. I can make that PR to this repo incase @ocavue is interested.

1 Like

If you could atleast share a simple repo or a gist with full code that would be really helpful, because in the @ocavue repo, when I inspected the list elements are not ul li at all. It seems to be a different logic all together.

Keyboard-only users is a good point! Thank you for pointing that.

We might still use Tab and Shift-Tab in our app, but it’s a good idea to set Mod-[ and Mod-] as default keyboard shortcuts in the open-source library.

I prefer to implement this will CSS, maybe something like this:

div.prosemirror-flat-list {
	list-style-type: square;

ul > ul,
div.prosemirror-flat-list > div.prosemirror-flat-list   {
	list-style-type: circle;

You can use CSS to set difference marker styles for, let’s say, 6 indent depths , and just give a fixed marker for any indent depths greater or equal than 7. We don’t usually need a very depth list anyway.

Synchronizing indent depth with ProseMirror seems too complex and might hurt performance in my option.

I’ve considered this approach too, but it doesn’t seems to match the ProseMirror’s tree data structure and might bring some unexpected complexity, so I didn’t choose in the end.

I am very glad that someone has implemented this method!

You should also make some copy-paste tests for at least Docs, Word, Notion and Apple Notes. Apple Notes uses some annoyingly weird schema.

Good point. I will do these tests. Thank you!

I can add this, but I’m not sure in which case people would want it.

Here is the source code for the online deme. It’s based on Remirror (instead of pure ProseMirror), but you should able to get the idea. I might also add a pure ProseMirror demo afterward.

the list elements are not ul li at all

This’s intended. By removing <ul> and replacing <li> with <div>, I “flat” the list structure and make certain things easier.

It seems to be a different logic all together.

You are right, it’s a complete rewrite compare to prosemirror-schema-list. However I hope to provide a familiar feeling for end-users.

1 Like

Thanks for the confirmation. But you mentioned that you will be implementing the list-style with CSS. But it cannot be matched to word/google docs implementation just with css right ? based on the indentation level circle / dot / square are alternating


Google Docs


You can write some not-so-long CSS to set markers separately for depths from 1 to 12, for example, set the marker as circle for depth 2, 5, 8 and 11. It’s a simple solution and good enough in 99.9% cases.

1 Like

Okay! How are you going to sync the start counters for ordered lists though? I think you’ll have to come up with something similar either way. But I’ll make some example repo so its easier for those interested to understand the context better.

1 Like

I’m using CSS rule counter-reset, counter-increment and counter-set for ordered lists, so I don’t need to use ProseMirror to manger the counter number.

Oh that’s pretty smart. I added a couple of issues I found by fiddling around. I’ll see when I get around cleaning up my experiment.