How to Use Tiptap Rich Text Editor with Next.js and Tailwind CSS: A Simple Guide

Theresa OkoroTheresa Okoro
8 min read

If you want a functional Rich Text Editor (RTE) that is simple to set up and use, you’ve come to the right place.

Tiptap is a headless editor framework with an open source core that allows you to build and customise your rich text editor effortlessly. The great thing about it is that it already gives you the essentials without requiring you to understand the low-level details making it an excellent choice for modern web applications.

I went on a rich text editor gold search and everywhere I searched, someone recommended TipTap as the top rich text editor. I used it, loved it, ran into some errrmm(not issues but my fuzzy brain did not necessary know certain things and how it should be adjusted especially if you are using TailwindCSS) so I'm here to give you the guide I wish I had.

WHY TIPTAP:

  • Ease of Use: Intuitive and easy to integrate into your project.

  • Customizable: Add or remove features as needed without delving into complex configurations.

  • Flexible Styling: Customize the styling of your Tiptap editor to create a unique design with minimal effort.

  • Easy-to-Use Documentation: Tiptap's documentation is straightforward and easy to follow.

Without further ado, let’s get into it.

SET UP A BASIC TIPTAP EDITOR

Create a project (optional)

If you don’t have an existing project, create one using the following command:

#create a project
npx create-next-app our-tiptap-project

# change directory
cd our-tiptap-project

Install dependencies

We need to install three packages: @tiptap/react, @tiptap/pm and @tiptap/starter-kit which includes all the extensions you need to get started quickly.

npm install @tiptap/react @tiptap/pm @tiptap/starter-kit

Create a Tiptap Component

In your project, create a new component file called Tiptap.jsx or Tiptap.tsx (if you're using TypeScript).

'use client'
import { useEditor, EditorContent } from '@tiptap/react'
import StarterKit from '@tiptap/starter-kit'
const Tiptap = () => {
  const editor = useEditor({
    extensions: [StarterKit],
    content: '<p>Welcome to Tiptap</p>',
  })
  return <EditorContent editor={editor} />
}
export default Tiptap

Add it to your app

Add your Tiptap component in your index file or your desired file.

import Tiptap from '../components/Tiptap'
export default function Home() {
  return <Tiptap />
}

Run “npm run dev” and go to your localhost to see the editor in action - open http://localhost:3000/

Welcome to the TipTap world, but hey, we can do more than that basic setup.

EXTENDING THE EDITOR WITH MORE FEATURES

Now, let’s make our TipTap editor more robust with fun things like making your text bold, italic, adding header, bullet list and ordered list. You can also easily add more functionality by looking at the doc.

INDEX.TSX

On our Tiptap component, we’re going to use two props and use useState to manage those props.

The initial content the user types in the input field will be stored in the jobDescription and the changes would be saved via setJobDescription]

  const [jobDescription, setJobDescription] = useState("");
   <Tiptap
    editorContent={jobDescription}
    onChange={setJobDescription}
  />

TIPTAP.TSX

Step 1: Define Your Props (TypeScript Only)

Add the types for your props if you're using TypeScript:

interface TipTapProps {
  editorContent: string;
  onChange: (content: string) => void;
}

Step 2: Initialise the Editor

This is where we can customise our rich text editor by adding the nodes (bulletlist, code block, emoji, heading, ordered list e.t.c), marks (bold, italics, strike, underline e.t.c) that we want to add to our app.

The StarterKit gives you a lot out of the box like bold, italics e.t.c, and in Tiptaps favourites words “Don’t bend it, extend it”, you can extend and add other items that you want like heading, bullet list, ordered list and so much more like we did below.

Content is the description we got from the input field a.k.a what the user types into the input field. You can also style the editor using editor props like we did below (so get your styling wizardry up or you can use my style below - easy and simple). The next is onUpdate, this simply helps us update what the user sees when they click on the button, this calls onChange and takes whatever is in the editor and convert it to HTML.

Are you still with me? I hope so, I told you that Tiptap gives you the ability to customise your Rich Text Editor easily so if you want to add Heading, you can configure it to the extensions like we did below, HTML attributes helps you to add class and to specify the levels you want (H1, H2 - I just used H2 here to simplify this).

We also specify in the below example, if the editor does not exist, we should not render the toolbar therefore returning “null”

const Tiptap = ({ editorContent, onChange }: TipTapProps) => {
  const editor = useEditor({
    extensions: [
      StarterKit,
      Heading.configure({
        HTMLAttributes: {
          class: "text-xl font-bold capitalize",
          levels: [2],
        },
      }),
      ListItem,
      BulletList.configure({
        HTMLAttributes: {
          class: "list-disc ml-2",
        },
      }),
      OrderedList.configure({
        HTMLAttributes: {
          class: "list-decimal ml-2",
        },
      }),
    ],
    immediatelyRender: false,
    editorProps: {
      attributes: {
        class:
          "shadow appearance-none min-h-[150px] border rounded w-full py-2 px-3 bg-white text-black text-sm mt-0 md:mt-3 leading-tight focus:outline-none focus:shadow-outline",
      },
    },
    content: editorContent,
    onUpdate: ({ editor }) => {
      onChange(editor.getHTML());
    },
  });
  if (!editor) {
    return null;
  }
return (TODO)
export default Tiptap;

STEP 3:

Now, let’s focus on what we would return.

Using the bold button as an example, if the button is clicked, we are telling the editor to use the chain method to confirm the focus to be on the bold button and to make the selected text bold and run it.

Now, lets add an active class to bold to give it a grey background when Active and remove the grey background when Inactive and don’t forget to specify that this is a button type.

Then add the editor to the EditorContent.

That’s the logic that can be applied to others.

 return (
    <div className="flex flex-col justify-stretch min-h-[200px] border rounded border-b-0">
      <div className="flex items-center gap-2 mb-2">
        {/* Bold Button */}
        <button
          type="button"
          onClick={() => editor.chain().focus().toggleBold().run()}
          className={`p-2 rounded ${
            editor.isActive("bold") ? "bg-gray-200" : ""
          }`}
          title="Bold (Ctrl+B)"
        >
          <b>B</b>
        </button>
        {/* Italic Button */}
        <button
          type="button"
          onClick={() => editor.chain().focus().toggleItalic().run()}
          className={`p-2 rounded ${
            editor.isActive("italic") ? "bg-gray-200" : ""
          }`}
          title="Italic (Ctrl+I)"
        >
          <i>I</i>
        </button>
        {/* Heading */}
        <button
          type="button"
          onClick={() =>
            editor.chain().focus().toggleHeading({ level: 2 }).run()
          }
          className={
            editor.isActive("heading", { level: 2 }) ? "is-active" : ""
          }
        >
          <Image
            src="/heading2.svg"
            alt="Heading 2"
            width={10}
            height={10}
            className="h-4 w-4"
          />
        </button>
        {/* Heading */}
        {/* Bullet List Button */}
        <button
          type="button"
          onClick={() => editor.chain().focus().toggleBulletList().run()}
          className={`p-2 rounded ${
            editor.isActive("bulletList") ? "bg-gray-200" : ""
          }`}
          title="Bullet List"
        >
          <Image
            src="/bullet-list.svg"
            alt="Bullet list"
            width={10}
            height={10}
            className="h-4 w-4"
          />
        </button>
        {/* Ordered List Button */}
        <button
          type="button"
          onClick={() => editor.chain().focus().toggleOrderedList().run()}
          className={`p-2 rounded ${
            editor.isActive("orderedList") ? "bg-gray-200" : ""
          }`}
          title="Ordered List"
        >
          <Image
            src="/numbered-list.svg"
            alt="Numberedlist"
            width={10}
            height={10}
            className="h-4 w-4"
          />
        </button>
      </div>
      {/* Editor Content */}
      <EditorContent editor={editor} />
    </div>
  );

Step 4: Extend Your Editor with More Extensions

Some extensions, such as Heading, BulletList, and others, require installation to be used. Be sure to check the documentation for detailed instructions on how to install and integrate them. Below are some useful links:

[ Heading Extension Documentation] (https://tiptap.dev/docs/editor/extensions/nodes/heading)

BulletList Extension Documentation

ListItem Extension Documentation

Additional Notes:

  • Heading Configuration: Simply installing and using the Heading extension won’t work out of the box. You’ll need to configure it and apply the necessary styles, as demonstrated in the example.

  • Tailwind CSS Integration: If you're using Heading, BulletList, and OrderedList with Tailwind CSS, you need to install the Tailwind Typography plugin. To make the listings and headings work, add the below configuration to your Extensions array (look at the full code example if you are confused). This helps to ensure that these elements render correctly. Here's a useful StackOverflow link that can guide you through the process.

Heading.configure({
        HTMLAttributes: {
          class: "text-xl font-bold capitalize",
          levels: [2],
        },
      }),
      BulletList.configure({
        HTMLAttributes: {
          class: "list-disc ml-2",
        },
      }),
      OrderedList.configure({
        HTMLAttributes: {
          class: "list-decimal ml-2",
        },
      }),

Here’s the full working code, feel free to customise and let me know what you think via the comment section.

import { useEditor, EditorContent } from "@tiptap/react";
import StarterKit from "@tiptap/starter-kit";
import BulletList from "@tiptap/extension-bullet-list";
import ListItem from "@tiptap/extension-list-item";
import OrderedList from "@tiptap/extension-ordered-list";
import Heading from "@tiptap/extension-heading";
import Image from "next/image";
interface TipTapProps {
  editorContent: string;
  onChange: (content: string) => void;
}
const Tiptap = ({ editorContent, onChange }: TipTapProps) => {
  const editor = useEditor({
    extensions: [
      StarterKit,
      ListItem,
      Heading.configure({
        HTMLAttributes: {
          class: "text-xl font-bold capitalize",
          levels: [2],
        },
      }),
      BulletList.configure({
        HTMLAttributes: {
          class: "list-disc ml-2",
        },
      }),
      OrderedList.configure({
        HTMLAttributes: {
          class: "list-decimal ml-2",
        },
      }),
    ],
    immediatelyRender: false,
    editorProps: {
      attributes: {
        class:
          "shadow appearance-none min-h-[150px] border rounded w-full py-2 px-3 bg-white text-black text-sm mt-0 md:mt-3 leading-tight focus:outline-none focus:shadow-outline",
      },
    },
    content: editorContent,
    onUpdate: ({ editor }) => {
      onChange(editor.getHTML());
    },
  });
  if (!editor) {
    return null;
  }
  return (
    <div className="flex flex-col justify-stretch min-h-[200px] border rounded border-b-0">
      <div className="flex items-center gap-2 mb-2">
        {/* Bold Button */}
        <button
          type="button"
          onClick={() => editor.chain().focus().toggleBold().run()}
          className={`p-2 rounded ${
            editor.isActive("bold") ? "bg-gray-200" : ""
          }`}
          title="Bold (Ctrl+B)"
        >
          <b>B</b>
        </button>
        {/* Italic Button */}
        <button
          type="button"
          onClick={() => editor.chain().focus().toggleItalic().run()}
          className={`p-2 rounded ${
            editor.isActive("italic") ? "bg-gray-200" : ""
          }`}
          title="Italic (Ctrl+I)"
        >
          <i>I</i>
        </button>
        {/* Heading */}
        <button
          type="button"
          onClick={() =>
            editor.chain().focus().toggleHeading({ level: 2 }).run()
          }
          className={
            editor.isActive("heading", { level: 2 }) ? "is-active" : ""
          }
        >
          <Image
            src="/heading2.svg"
            alt="Heading 2"
            width={10}
            height={10}
            className="h-4 w-4"
          />
        </button>
        {/* Heading */}
        {/* Bullet List Button */}
        <button
          type="button"
          onClick={() => editor.chain().focus().toggleBulletList().run()}
          className={`p-2 rounded ${
            editor.isActive("bulletList") ? "bg-gray-200" : ""
          }`}
          title="Bullet List"
        >
          <Image
            src="/bullet-list.svg"
            alt="Bullet list"
            width={10}
            height={10}
            className="h-4 w-4"
          />
        </button>
        {/* Ordered List Button */}
        <button
          type="button"
          onClick={() => editor.chain().focus().toggleOrderedList().run()}
          className={`p-2 rounded ${
            editor.isActive("orderedList") ? "bg-gray-200" : ""
          }`}
          title="Ordered List"
        >
          <Image
            src="/numbered-list.svg"
            alt="Numberedlist"
            width={10}
            height={10}
            className="h-4 w-4"
          />
        </button>
      </div>
      {/* Editor Content */}
      <EditorContent editor={editor} />
    </div>
  );
};
export default Tiptap;

Wrapping Up:

Now that you’ve set up and customized your Tiptap Rich Text Editor, feel free to explore more extensions and adjust them to your needs. This guide provides a basic understanding, but the possibilities are endless with Tiptap. Whether you want to add custom nodes, marks, or even your own extensions, the documentation is your best friend.

If you have any questions, feel free to drop them in the comments. I hope this guide was helpful in getting you started with Tiptap in your Next.js and Tailwind CSS project. Happy coding!

11
Subscribe to my newsletter

Read articles from Theresa Okoro directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Theresa Okoro
Theresa Okoro