Alpine

The following guide describes how to integrate Tiptap with version 3 of Alpine.js. For the sake of this guide, we'll use Vite to quickly set up a project, but you can use whatever you're used to. Vite is just really fast and we love it!

Requirements

Create a project (optional)

If you already have an Alpine.js project, that's fine too. Just skip this step.

For the purpose of this guide, start with a fresh Vite project called my-tiptap-project. Vite sets up everything we need, just select the Vanilla JavaScript template.

npm init vite@latest my-tiptap-project -- --template vanilla
cd my-tiptap-project
npm install
npm run dev

Install the dependencies

Okay, enough of the boring boilerplate work. Let's finally install Tiptap! For the following example, you'll need alpinejs, the @tiptap/core package, the @tiptap/pm package, and the @tiptap/starter-kit, which includes the most common extensions to get started quickly.

npm install alpinejs @tiptap/core @tiptap/pm @tiptap/starter-kit

If you followed step 1, you can now start your project with npm run dev, and open http://localhost:5173 in your favorite browser. This might be different if you're working with an existing project.

Integrate Tiptap

To actually start using Tiptap, you'll need to write a little bit of JavaScript. Let's put the following example code in a file called main.js.

This is the fastest way to get Tiptap up and running with Alpine.js. It will give you a very basic version of Tiptap. No worries, you will be able to add more functionality soon.

import Alpine from 'alpinejs'
import { Editor } from '@tiptap/core'
import StarterKit from '@tiptap/starter-kit'

document.addEventListener('alpine:init', () => {
  Alpine.data('editor', (content) => {
    let editor // Alpine's reactive engine automatically wraps component properties in proxy objects. If you attempt to use a proxied editor instance to apply a transaction, it will cause a "Range Error: Applying a mismatched transaction", so be sure to unwrap it using Alpine.raw(), or simply avoid storing your editor as a component property, as shown in this example.

    return {
      updatedAt: Date.now(), // force Alpine to rerender on selection change
      init() {
        const _this = this

        editor = new Editor({
          element: this.$refs.element,
          extensions: [StarterKit],
          content: content,
          onCreate({ editor }) {
            _this.updatedAt = Date.now()
          },
          onUpdate({ editor }) {
            _this.updatedAt = Date.now()
          },
          onSelectionUpdate({ editor }) {
            _this.updatedAt = Date.now()
          },
        })
      },
      isLoaded() {
        return editor
      },
      isActive(type, opts = {}) {
        return editor.isActive(type, opts)
      },
      toggleHeading(opts) {
        editor.chain().toggleHeading(opts).focus().run()
      },
      toggleBold() {
        editor.chain().focus().toggleBold().run()
      },
      toggleItalic() {
        editor.chain().toggleItalic().focus().run()
      },
    }
  })
})

window.Alpine = Alpine
Alpine.start()

Add it to your app

Now, let's replace the contents of index.html with the following example code to use the editor in our app.

<!doctype html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
  </head>
  <body>
    <div x-data="editor('<p>Hello world! :-)</p>')">
      <template x-if="isLoaded()">
        <div class="menu">
          <button
            @click="toggleHeading({ level: 1 })"
            :class="{ 'is-active': isActive('heading', { level: 1 }, updatedAt) }"
          >
            H1
          </button>
          <button @click="toggleBold()" :class="{ 'is-active' : isActive('bold', updatedAt) }">
            Bold
          </button>
          <button @click="toggleItalic()" :class="{ 'is-active' : isActive('italic', updatedAt) }">
            Italic
          </button>
        </div>
      </template>

      <div x-ref="element"></div>
    </div>

    <script type="module" src="/main.js"></script>

    <style>
      body {
        margin: 2rem;
        font-family: sans-serif;
      }
      button.is-active {
        background: black;
        color: white;
      }
      .tiptap {
        padding: 0.5rem 1rem;
        margin: 1rem 0;
        border: 1px solid #ccc;
      }
    </style>
  </body>
</html>

Tiptap should now be visible in your browser. Time to give yourself a pat on the back! :)