Svelte

Learn how to integrate Tiptap with your SvelteKit project using this step-by-sep guide. Alternatively, check out our Svelte text editor example.

Take a shortcut: Svelte REPL with Tiptap

If you want to jump into it right away, here is a Svelte REPL with Tiptap.

Requirements

  • Node installed on your machine
  • Experience with Svelte

Create a project (optional)

If you already have a SvelteKit project, that's fine too. Just skip this step.

For the purpose of this guide, start with a fresh SvelteKit project called my-tiptap-project. The following commands set up everything we need. It asks a lot of questions, but select your preferred options or use the defaults.

npm create svelte@latest my-tiptap-project
cd my-tiptap-project
npm install
npm run dev

Install dependencies

Now that we're done with boilerplate, let's install Tiptap! For the following example you'll need the @tiptap/core package, with a few components, @tiptap/pm, and @tiptap/starter-kit, which includes the most common extensions to get started quickly.

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

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

Integrate Tiptap

To start using Tiptap, you'll need to add a new component to your app. Let's call it Tiptap and add the following example code in src/lib/Tiptap.svelte.

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

import { Editor } from '@tiptap/core'
  import { StarterKit } from '@tiptap/starter-kit'
  import BubbleMenu from '@tiptap/extension-bubble-menu'

  let bubbleMenu = $state()
  let element = $state()
  let editorState = $state({editor: null})

  onMount(() => {
    editor = new Editor({
      element: element,
      extensions: [
        StarterKit,
        BubbleMenu.configure({
          element: bubbleMenu,
        }),
      ],
      content: `
        <h1>Hello Svelte! 🌍️ </h1>
        <p>This editor is running in Svelte.</p>
        <p>Select some text to see the bubble menu popping up.</p>
      `,
      onTransaction: ({editor}) => {
        // force re-render so `editor.isActive` works as expected
		editorState = { editor }
      },
    })
  })
  onDestroy(() => {
    editor.destroy()
  })
</script>

<div style="position: relative" class="app">
  {#if editorState.editor}
    <div class="fixed-menu">
      <button
        onclick={() => editorState.editor.chain().focus().toggleHeading({ level: 1 }).run()}
        class:active={editorState.editor.isActive('heading', { level: 1 })}
      >
        H1
      </button>
      <button
        onclick={() => editorState.editor.chain().focus().toggleHeading({ level: 2 }).run()}
        class:active={editorState.editor.isActive('heading', { level: 2 })}
      >
        H2
      </button>
      <button onclick={() => editorState.editor.chain().focus().setParagraph().run()} class:active={editorState.editor.isActive('paragraph')}>
        P
      </button>
    </div>
  {/if}

  <div bind:this={element}></div>
</div>

<style>
  button.active {
    background: black;
    color: white;
  }
</style>

Add it to your app

Now, let's replace the content of src/routes/+page.svelte with the following example code to use our new Tiptap component in our app.

<script>
  import Tiptap from '$lib/Tiptap.svelte'
</script>

<main>
  <Tiptap />
</main>

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

Next steps

Setup with Svelte legacy syntax

The example above uses the Svelte runes syntax. If your Svelte app is in legacy mode, use this code for the Tiptap.svelte component instead.

<script>
	import { onMount, onDestroy } from 'svelte';
	import { Editor } from '@tiptap/core';
	import StarterKit from '@tiptap/starter-kit';

	let element;
	let editor;

	onMount(() => {
		editor = new Editor({
			element: element,
			extensions: [StarterKit],
			content: '<p>Hello World! 🌍️ </p>',
			onTransaction: () => {
				// force re-render so `editor.isActive` works as expected
				editor = editor;
			},
		});
	});

	onDestroy(() => {
		if (editor) {
			editor.destroy();
		}
	});
</script>

{#if editor}
	<button
		on:click={() => editor.chain().focus().toggleHeading({ level: 1 }).run()}
		class:active={editor.isActive('heading', { level: 1 })}
	>
		H1
	</button>
	<button
		on:click={() => editor.chain().focus().toggleHeading({ level: 2 }).run()}
		class:active={editor.isActive('heading', { level: 2 })}
	>
		H2
	</button>
	<button
		on:click={() => editor.chain().focus().setParagraph().run()}
		class:active={editor.isActive('paragraph')}
	>
		P
	</button>
{/if}

<div bind:this={element} />

<style>
	button.active {
		background: black;
		color: white;
	}
</style>