How do I stop TipTap adding p tags to my li items?

Just started experimenting with V16 and finding that TipTap is adding p tags to all my li items. Is there a way to stop this?

thanks, tom

Oh… looks like it may have pitfalls? Could you instead remove at render time with a custom htmlAgility pack parser… though could have issues when <p> are required?
Alternatively you could use a block in the rte to insert a repeatable textstring.. and then control the render?

Here’s Perplexity’s take…

ProseMirror (tiptap 's underlying editor framework) insists on putting <p> (paragraph) tags inside <li> (list items) because of how its schema is designed and how it handles document structure. In ProseMirror’s schema for list items, the content model for a list item is defined to expect block content, commonly starting with a paragraph. This is to maintain consistency and full editing capabilities like splitting, wrapping, and nesting within list items.

The main reasons are:

  1. Schema Design & Editing Consistency: ProseMirror requires list items to contain block nodes (e.g., paragraphs) rather than inline text directly so that commands like splitting the list item or adding new ones will work correctly. Lists in ProseMirror rely on a text block structure inside the
  2. to maintain consistent behavior during user input and editing.
  3. Editing Commands Dependence: Commands like splitListItem expect the list item to have at least one block-level text node such as a paragraph at its root. Removing the paragraph or switching to inline-only content breaks or complicates default list commands.
  4. HTML Specification Context: The HTML spec itself allows list items to contain block-level elements including paragraphs, so wrapping content in <p> inside <li> is valid HTML. Although simple lists sometimes have direct text inside <li>, the spec permits block content to maintain semantic clarity.
  5. Practical Editing Considerations: Wrapping each list item’s content in a paragraph helps visually separate lines, preserve spacing, and control editing caret movements, which are important in rich text editors.

So, ProseMirror’s insistence on paragraphs inside list items is a design choice aligned with both editing needs and valid HTML semantics. Although it’s not mandatory in all HTML usage to have <p> inside <li>, it is the default block content model for most rich text editors to ensure predictable and flexible editing

Thanks Mike, it’s one of those things we’ll just have to get used to. I had wanted to get it to work like tinyMCE and thought there might be a configuration option or an extension already created to prevent it adding tags to my markup. I can see this breaking lots of styling initially :frowning:

I found another option, but it looks like it might then prevent list items containing block elements: (from Claude Desktop)

changing content: 'paragraph block*' (the default) to content: 'text*' tells TipTap that list items should only contain text nodes, not paragraph blocks.

import ListItem from '@tiptap/extension-list-item'

const CustomListItem = ListItem.extend({
  addAttributes() {
    return {}
  },
  parseHTML() {
    return [
      {
        tag: 'li',
        getContent(node, schema) {
          const content = []
          node.childNodes.forEach(child => {
            if (child.nodeName === 'P') {
              child.childNodes.forEach(textNode => {
                content.push(schema.text(textNode.textContent))
              })
            }
          })
          return Fragment.from(content)
        }
      }
    ]
  }
})

@TwoMoreThings Did you actually use this snippet? Did it work? How did you hook it up to be used by TipTap in the backoffice?

Sorry Markus, I didn’t manage to get this working. It looks like you can add a TipTap extension by creating an Umbraco package, at least according to this page in the docs, but that’s not something I managed to get working, not being familiar with Vite etc.

We ran into this as well. Our current take is “that’s weird, but ok.” I don’t like li lists containing p tags, but I guess it doesn’t matter.

Personally, after reading up on it, I’d say like @asawyer

I see that both are valid HTML, the output is a little different though. If it breaks some of the output, a quick fix to mimic previous behavior would be:

li > p { 
  margin: 0; 
  display: inline; 
}

Also, I do understand that if you’re upgrading it will be a while before you discover this new behavior. An editor makes a list and suddenly it looks different from all the other lists they made before.

We could change the defaults, but this has one very unfortunate side-effect: you’d never be able to use <p> elements in lists again (they would just be stripped). But that is valid HTML and has legitimate use cases. Using <p> inside <li> was also something you could already achieve in TinyMCE, so it would break things in different interesting ways which we’d need to find weird workarounds for :sweat_smile:

Just for perspective, this is what two LLMs that I consulted had to say about it:

TipTap uses ProseMirror’s schema. ProseMirror intentionally encourages:

<li>
  <p>…</p>
</li>

because:

  • a block-level node (like <p>) makes editing consistent
  • it avoids weird cursor behaviour
  • it allows adding multiple paragraphs inside one list item
  • it matches the HTML spec perfectly
  • TinyMCE doesn’t do this by default because it’s older and tries to emulate “Word-like” content, not necessarily semantic correctness.

So is <p> inside <li> more correct?
Yes. It is more semantically flexible and 100% valid HTML.

Plain text directly inside <li> is also valid, but wrapping content in a <p>:

  • better reflects that it’s a block of text
  • allows structured content inside a list item
  • aligns with modern editors
  • avoids ambiguous whitespace rules

This is why modern editors (TipTap, ProseMirror, CKEditor 5) use <p> inside <li>.

I did some research around this and came to the following conclusion.

The default implementation of UL/OL in CodeMirror (which Tiptap uses under the hood) forces the use of a <p> element. There’s not simple configuration to disable this.

There are a couple of options

  1. Just accept it
    This would save you a lot of time :slight_smile:

  2. Hack the “Ordered List” plugin for Tiptap.
    This would require custom JavaScript to remove the built in Tiptap plugin for “List” and replace it with a custom one. This thread has some examples. However, the hack disables the possibility to create nested UL/OL lists in the editor which is not great. I did try this but it just felt wrong.

  3. Roll your own
    It would probably be technically possible to re-implement the “List” plugin for Tiptap (and CodeMirror) and replace the backoffice plugin that so that your custom implementation is used. CodeMirror feels complicated and I did not have days to invest in this so I did not go down this path.

What did I do?
I spent around 4 hours trying to get around it but ended up with “option” number 1 :slight_smile:

If anyone ever finds a solution that actually works with nested ULs I would be very interested to know more.

:rocket: