Vanilla JavaScript

Are you building without a frontend framework like React or Vue? No problem, you can use Tiptap directly in plain JavaScript.

Hint

"Vanilla JavaScript" here means no frontend framework, but you are still using modern JavaScript with ES module imports (e.g. through Vite, Rollup, or Webpack).

If you'd rather skip build tools entirely, check the CDN section below for a setup that runs directly in the browser.

With a build tool (Vite, Webpack, Rollup)

This approach is recommended if you're already using a bundler or just want the standard npm workflow.

Install dependencies

You'll need the core Tiptap packages to get started:

  • @tiptap/core – the main editor API
  • @tiptap/pm – ProseMirror, the engine behind Tiptap
  • @tiptap/starter-kit – a convenient bundle of common extensions
npm install @tiptap/core @tiptap/pm @tiptap/starter-kit

Add markup

Add a simple container in your HTML where you want the editor to appear:

<div class="element"></div>

Initialize the editor

Create a JavaScript file (for example src/main.js) and add the following code:

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

new Editor({
  element: document.querySelector('.element'),
  extensions: [StarterKit],
  content: '<p>Hello World!</p>',
})

Now run your dev server (for example with Vite: npm run dev) and open the page in your browser. You should see a working Tiptap editor.


Without a build tool (CDN)

If you'd rather skip npm and build tools entirely, you can load Tiptap directly from a CDN using ES module imports. This works in all modern browsers.

Note

Your <script> tag must include type="module" for the import syntax to work in the browser.

Minimal example

<script type="module">
  import { Editor } from 'https://esm.sh/@tiptap/core'
  import StarterKit from 'https://esm.sh/@tiptap/starter-kit'

  new Editor({
    element: document.querySelector('.element'),
    extensions: [StarterKit],
    content: '<p>Hello from CDN!</p>',
  })
</script>

<div class="element"></div>

Complete HTML file with toolbar

Here's a copy-paste ready example that includes a toolbar with formatting buttons:

<!doctype html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Tiptap Editor</title>
    <style>
      body {
        margin: 2rem;
        font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
      }
      .editor-container {
        max-width: 48rem;
        margin: 0 auto;
      }
      .toolbar {
        display: flex;
        gap: 0.25rem;
        padding: 0.5rem;
        border: 1px solid #d1d5db;
        border-bottom: none;
        border-radius: 0.375rem 0.375rem 0 0;
        background: #f9fafb;
        flex-wrap: wrap;
      }
      .toolbar button {
        padding: 0.25rem 0.5rem;
        font-size: 0.875rem;
        border: 1px solid transparent;
        border-radius: 0.25rem;
        background: transparent;
        cursor: pointer;
      }
      .toolbar button:hover {
        background: #e5e7eb;
      }
      .toolbar button.is-active {
        background: #111827;
        color: #fff;
      }
      .tiptap {
        padding: 0.75rem 1rem;
        border: 1px solid #d1d5db;
        border-radius: 0 0 0.375rem 0.375rem;
        min-height: 10rem;
        outline: none;
      }
      .tiptap p { margin: 0.5rem 0; }
      .tiptap h1, .tiptap h2, .tiptap h3 { margin: 1rem 0 0.5rem; }
      .tiptap ul, .tiptap ol { padding-left: 1.5rem; }
      .tiptap pre {
        background: #111827;
        color: #f3f4f6;
        padding: 0.75rem;
        border-radius: 0.25rem;
        font-size: 0.875rem;
      }
      .tiptap code {
        background: #f3f4f6;
        padding: 0.125rem 0.25rem;
        border-radius: 0.25rem;
        font-size: 0.875rem;
      }
      .tiptap pre code {
        background: transparent;
        padding: 0;
      }
    </style>
  </head>
  <body>
    <div class="editor-container">
      <div class="toolbar" id="toolbar">
        <button data-tiptap-button="bold">Bold</button>
        <button data-tiptap-button="italic">Italic</button>
        <button data-tiptap-button="strike">Strike</button>
        <button data-tiptap-button="code">Code</button>
        <button data-tiptap-button="h1">H1</button>
        <button data-tiptap-button="h2">H2</button>
        <button data-tiptap-button="bulletList">Bullet List</button>
        <button data-tiptap-button="orderedList">Ordered List</button>
        <button data-tiptap-button="blockquote">Blockquote</button>
        <button data-tiptap-button="codeBlock">Code Block</button>
      </div>
      <div id="editor"></div>
    </div>

    <script type="module">
      import { Editor } from 'https://esm.sh/@tiptap/core'
      import StarterKit from 'https://esm.sh/@tiptap/starter-kit'

      const editor = new Editor({
        element: document.querySelector('#editor'),
        extensions: [StarterKit],
        content: `
          <h1>Welcome to Tiptap</h1>
          <p>This is a text editor built with <strong>Vanilla JavaScript</strong> and loaded from a CDN.</p>
          <p>Try the toolbar buttons above to format your text.</p>
        `,
      })

      // Wire up toolbar buttons
      const buttons = document.querySelectorAll('[data-tiptap-button]')
      buttons.forEach((button) => {
        button.addEventListener('click', () => {
          const command = button.dataset.tiptapButton

          switch (command) {
            case 'bold':
              editor.chain().focus().toggleBold().run()
              break
            case 'italic':
              editor.chain().focus().toggleItalic().run()
              break
            case 'strike':
              editor.chain().focus().toggleStrike().run()
              break
            case 'code':
              editor.chain().focus().toggleCode().run()
              break
            case 'h1':
              editor.chain().focus().toggleHeading({ level: 1 }).run()
              break
            case 'h2':
              editor.chain().focus().toggleHeading({ level: 2 }).run()
              break
            case 'bulletList':
              editor.chain().focus().toggleBulletList().run()
              break
            case 'orderedList':
              editor.chain().focus().toggleOrderedList().run()
              break
            case 'blockquote':
              editor.chain().focus().toggleBlockquote().run()
              break
            case 'codeBlock':
              editor.chain().focus().toggleCodeBlock().run()
              break
          }

          updateActiveButtons()
        })
      })

      function updateActiveButtons() {
        const map = {
          bold: () => editor.isActive('bold'),
          italic: () => editor.isActive('italic'),
          strike: () => editor.isActive('strike'),
          code: () => editor.isActive('code'),
          h1: () => editor.isActive('heading', { level: 1 }),
          h2: () => editor.isActive('heading', { level: 2 }),
          bulletList: () => editor.isActive('bulletList'),
          orderedList: () => editor.isActive('orderedList'),
          blockquote: () => editor.isActive('blockquote'),
          codeBlock: () => editor.isActive('codeBlock'),
        }

        buttons.forEach((button) => {
          const command = button.dataset.tiptapButton
          const check = map[command]
          if (check && check()) {
            button.classList.add('is-active')
          } else {
            button.classList.remove('is-active')
          }
        })
      }

      editor.on('selectionUpdate', updateActiveButtons)
      editor.on('update', updateActiveButtons)
    </script>
  </body>
</html>

Save this as index.html and open it in any modern browser — no server required.

Live demo

You can see a similar working example on CodeSandbox:

https://codesandbox.io/s/throbbing-smoke-now37q


Wiring a toolbar (plain JS)

Tiptap is headless, so it doesn't ship with a built-in toolbar. You need to wire up your own UI buttons. Here is the pattern in plain JavaScript:

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

const editor = new Editor({
  element: document.querySelector('#editor'),
  extensions: [StarterKit],
  content: '<p>Hello World!</p>',
})

// Listen for clicks and call editor commands
document.querySelector('#bold-button').addEventListener('click', () => {
  editor.chain().focus().toggleBold().run()
})

// Check if a mark or node is active to style the button
const isBold = editor.isActive('bold')
const isHeading1 = editor.isActive('heading', { level: 1 })
const isBulletList = editor.isActive('bulletList')

// Update button active state on selection change
editor.on('selectionUpdate', () => {
  document.querySelector('#bold-button').classList.toggle('is-active', editor.isActive('bold'))
})

Common editor commands

CommandCode
Boldeditor.chain().focus().toggleBold().run()
Italiceditor.chain().focus().toggleItalic().run()
Strikeeditor.chain().focus().toggleStrike().run()
Code (inline)editor.chain().focus().toggleCode().run()
Heading 1editor.chain().focus().toggleHeading({ level: 1 }).run()
Heading 2editor.chain().focus().toggleHeading({ level: 2 }).run()
Bullet Listeditor.chain().focus().toggleBulletList().run()
Ordered Listeditor.chain().focus().toggleOrderedList().run()
Blockquoteeditor.chain().focus().toggleBlockquote().run()
Code Blockeditor.chain().focus().toggleCodeBlock().run()
Undoeditor.chain().focus().undo().run()
Redoeditor.chain().focus().redo().run()

Refer to the individual extension docs for available commands.


Styling

Tiptap doesn't include visual styles by default — it only outputs semantic HTML. You can style it however you like with your own CSS or a framework such as Tailwind or Bootstrap.

The StarterKit includes minimal default styles that make the text render like a basic document.

Learn more in the Styling guide.

Next steps