---
title: "Vanilla JavaScript"
description: "Learn how to set up the Tiptap Editor with Vanilla JavaScript, install dependencies and initialize the editor in the docs!"
canonical_url: "https://tiptap.dev/docs/editor/getting-started/install/vanilla-javascript"
---

# Vanilla JavaScript

Learn how to set up the Tiptap Editor with Vanilla JavaScript, install dependencies and initialize the editor in the docs!

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](#without-a-build-tool-cdn) 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

```bash
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:

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

### Initialize the editor

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

```js
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

```html
<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:

```html
<!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](https://codesandbox.io/s/throbbing-smoke-now37q?file=/index.html)

---

## 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:

```js
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

| Command       | Code                                                       |
| ------------- | ---------------------------------------------------------- |
| Bold          | `editor.chain().focus().toggleBold().run()`                |
| Italic        | `editor.chain().focus().toggleItalic().run()`              |
| Strike        | `editor.chain().focus().toggleStrike().run()`              |
| Code (inline) | `editor.chain().focus().toggleCode().run()`                |
| Heading 1     | `editor.chain().focus().toggleHeading({ level: 1 }).run()` |
| Heading 2     | `editor.chain().focus().toggleHeading({ level: 2 }).run()` |
| Bullet List   | `editor.chain().focus().toggleBulletList().run()`          |
| Ordered List  | `editor.chain().focus().toggleOrderedList().run()`         |
| Blockquote    | `editor.chain().focus().toggleBlockquote().run()`          |
| Code Block    | `editor.chain().focus().toggleCodeBlock().run()`           |
| Undo          | `editor.chain().focus().undo().run()`                      |
| Redo          | `editor.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](https://tiptap.dev/docs/editor/getting-started/style-editor.md).

## Next steps

- [Configure your editor](https://tiptap.dev/docs/editor/getting-started/configure.md)
- [Add styles to your editor](https://tiptap.dev/docs/editor/getting-started/style-editor.md)
- [Learn more about Tiptaps concepts](https://tiptap.dev/docs/editor/core-concepts/introduction.md)
- [Learn how to persist the editor state](https://tiptap.dev/docs/editor/core-concepts/persistence.md)
- [Start building your own extensions](https://tiptap.dev/docs/editor/extensions/custom-extensions.md)
