I’ve been using Mantine in one of my side projects and had the need for a rich text editor. At the time, Mantine used an editor component based on Quill.js, which I have had issues with in the past. I also, at the time, wanted to use Markdown for my storage format, I’ve recently decided against that though. More recently, Mantine has swapped to an editor component based on Tiptap. I had read a lot about Tiptap recently, but hadn’t used it. Now that I have, in Mantine, I’d like to share how it can be used in a form and renders.

When I turned my back on the original Mantine RTE, I built a Markdown editor, based on Draft.js. I love the editor I built, it works perfectly and does exactly what I wanted. I built it as a Mantine component, with all of the Styles API builtin, and everything. The only thing that made me second guess my editor is that Draft.js is out of development and will not be developed further. It’s not an issue now, but eventually it will break, and I need something I can use as long as I need. As I began to second guess, I also questioned whether storing content as Markdown would be a good choice in the long run. I don’t trust HTML as input and I don’t want to have to worry about filtering it any more than necessary.

The more I thought about it, the more I didn’t want to use Markdown. But I had built this editor that exclusively did Markdown. It wouldn’t have been all that big of a shift to make it use JSON, then I’d have to worry about utilities for rendering. I wasn’t wanting to expand the needed dependencies too much. I decided to look at the Mantine RTE package I’d seen again and noticed it wasn’t the same one. It is now built on TipTap, which is in active development and easily supports JSON as input and output already.

Tiptap is relatively easy to set up in Mantine, there’s a page of docs to get you started. The issue, though, is it doesn’t show you how to use it in a form or how to output it. Output is the easiest, so we’ll look at that first:

Formatting Output

The useEditor hook from the @tiptap/react package has an onUpdate method that is called as the content changes. What I’ve done is I pass my form object and the form property name of my input into my editor component, so form can be called onUpdate().

import { useEditor } from '@tiptap/react';
import Highlight from '@tiptap/extension-highlight';
import StarterKit from '@tiptap/starter-kit';
import Underline from '@tiptap/extension-underline';
import TextAlign from '@tiptap/extension-text-align';
import Superscript from '@tiptap/extension-superscript';
import SubScript from '@tiptap/extension-subscript';
... // other code
export default function Default({ name, form }) {
...
  const editor = useEditor({
    extensions: [
      StarterKit,
      Underline,
      Link,
      Superscript,
      SubScript,
      Highlight,
      TextAlign.configure({ types: ['heading', 'paragraph'] })
    ],
    content: form.values[name],
    onUpdate({ editor }) {
      form.setFieldValue(name, editor?.getJSON());
    }
  });
...

The above will update form.values[name] to be equal to editor.getJSON() on any update in the editor. This means the form field is always up to date and you can save it at any time. If you prefer HTML to JSON, you can do the same with editor.getHTML(). See the Tiptap Output docs for more information.

Rendering the Content

Storing the content is fairly easy, but how so we display it? I’m storing my content as JSON, which comes out something like:

"text": {
    "content": [
      {
        "type": "paragraph",
        "attrs": {
          "textAlign": "left"
        },
        "content": [
          {
            "type": "text",
            "text": "Welcome to my blog :)"
          }
        ]
      },
      {
        "type": "heading",
        "attrs": {
          "textAlign": "left",
          "level": 1
        },
        "content": [
          {
            "type": "text",
            "text": "Nice!"
          }
        ]
      }
    ],
    "type": "doc"
  },

I chose this storage format, because it can be programmatically updated and I can use it as data, however I need, before I convert it to output. Unfortunately, that means I have to reformat it. Fortunately, Mantine and Tiptap make this easy. To dynamically convert Tiptap JSON to Mantine style HTML on the page, just use something like this:

import { TypographyStylesProvider } from '@mantine/core';
import { generateHTML, JSONContent } from '@tiptap/core';
import Highlight from '@tiptap/extension-highlight';
import StarterKit from '@tiptap/starter-kit';
import SubScript from '@tiptap/extension-subscript';
import Superscript from '@tiptap/extension-superscript';
import TextAlign from '@tiptap/extension-text-align';
import Underline from '@tiptap/extension-underline';

export default function HTMLContent({
  content
}: {
  content: { type?: string; content?: JSONContent[] | undefined };
}) {
  return (
    <TypographyStylesProvider>
      <div
        dangerouslySetInnerHTML={{
          __html: generateHTML(content, [
            StarterKit,
            Highlight,
            SubScript,
            Superscript,
            TextAlign,
            Underline
          ])
        }}
      />
    </TypographyStylesProvider>
  );
}

Note: Most of my examples are Javascript, but I’ve decided to keep this one as Type Script, as that JSONContent interface is useful. If you aren’t using Type Script, just remove the content type.

The benefit to this method is you get the Mantine styling, without having to worry about styling each tag in the generated HTML. If you are just rendering stored HTML, you can simplify it to this:

import { TypographyStylesProvider } from '@mantine/core';

export default function HTMLContent({ content } {
  return (
    <TypographyStylesProvider>
      <div
        dangerouslySetInnerHTML={{
          __html: content
        }}
      />
    </TypographyStylesProvider>
  );
}

Now you would use this component like this (for either of the two examples above):

<HTMLContent content={data?.text} />

You could also use the children prop to use it between the tags, instead of the content prop, if you wanted. This way just seemed simpler.

So that’s how you use the input and output content from the Tiptap editor component in Mantine. It’s by far the easiest editor I’ve tried with Mantine. Just follow the Tiptap docs on Mantine.dev and add what I’ve shown above. As I had created my own editor previously, I had a specific style in mind, so I’ve also customized the editor. I’ll add another tutorial for using the Styles API with Mantine’s RTE sometime.