Integrate Tiptap with Svelte
A Complete Step-by-Step Guide
I found it difficult to integrate the popular text editor Tiptap into my Svelte project due to the lack of information in the official Tiptap documentation. This tutorial will cover the missing pieces and provide a comprehensive guide on how to get Tiptap working with Svelte.
Published Aug 6, 2025 • 4 minutes read

Introduction
When building my Svelte project, I needed a powerful, extensible rich text editor that could support customization and modern UX features. After researching available editors, I chose Tiptap because of its flexibility, active development, and modern ProseMirror-based architecture.
However, integrating Tiptap into a Svelte environment was not as straightforward as I had hoped. While the official documentation is quite comprehensive for React and Vue, it lacks guidance for Svelte users. This tutorial aims to fill in the gaps and help others get Tiptap working smoothly in their Svelte projects.
Why the Existing Tiptap Documentation Falls Short
Here are a few pain points I encountered:
No example of how to pass data in and out of the editor
Managing bindings between Tiptap's internal state and Svelte's reactivity is not covered in the official guide.Errors when adding buttons to control formatting
Trying to add custom buttons caused unexpected issues due to how Tiptap handles commands and editor state.Lack of guidance on styling the control panel
There’s little help on how to style your control buttons and panel, or how to reflect active formatting state in the UI.
This tutorial addresses all of these challenges.
Installation and Initialization
Follow the official Tiptap guide to get started with installation:
Let the Data Flow
Here’s how to pass data in and retrieve updates from Tiptap in a reactive way.
Example: Two-way Binding
The following is the example code Tiptap.svelte
of how we binding the data of the editor:
<script lang="ts">
import "./Tiptap-styles.scss";
import StarterKit from "@tiptap/starter-kit";
import { Editor } from "@tiptap/core";
import { onMount, onDestroy } from "svelte";
import { TableKit } from '@tiptap/extension-table'
/** in/out */
export let html: string;
/** out only */
export let editor: Editor;
let rootEl: HTMLDivElement;
let lastHTML: string | null = null;
onMount(onLoad);
function onLoad() {
createEditor();
}
function createEditor() {
editor = new Editor({
element: rootEl,
extensions: [
StarterKit, TableKit
],
content: html,
onTransaction: () => {
// force re-render so `editor.isActive` works as expected
editor = editor;
},
onUpdate: ({ editor }) => {
html = lastHTML = editor.getHTML();
},
editable: true
});
lastHTML = html;
}
export function forceReload() {
if (editor) {
editor.destroy();
}
createEditor();
}
// html changed by caller, not editor
$: html != lastHTML && forceReload();
onDestroy(() => {
if (editor) {
editor.destroy();
}
});
</script>
<div bind:this={rootEl} class="html-editor"></div>
Adding Control Panel
Problem: Errors When Adding Buttons
When I first added control buttons (e.g., bold, italic), I ran into this error:
The editor view is not available. Cannot access view['hasFocus']. The editor may not be mounted yet.
in <unknown>
in Tiptap.svelte
in +page.svelte
in +layout.svelte
in root.svelte
Then, I realize the issue is caused by this line of code:
disabled={!editor.can().chain().focus().toggleBold().run()}
The error occurs because we are calling editor.can() methods before the editor is fully initialized and mounted. The solution is to either remove the disabled attribute from the button or add additional check for editor readiness.
Example: Custom Control Panel
Add following code before the html-editor
:
{#if editor}
<div class="control-group">
<!-- Text formatting -->
<div class="button-group">
<button
type="button"
on:click={() => editor.chain().focus().toggleBold().run()}
class={editor.isActive("bold") ? "is-active" : ""}
>
Bold
</button>
<button
type="button"
on:click={() => editor.chain().focus().toggleItalic().run()}
class={editor.isActive("italic") ? "is-active" : ""}
>
Italic
</button>
<button
type="button"
on:click={() => editor.chain().focus().toggleStrike().run()}
class={editor.isActive("strike") ? "is-active" : ""}
>
Strike
</button>
<button
on:click={() => editor.chain().focus().toggleCode().run()}
class={editor.isActive("code") ? "is-active" : ""}
>
Code
</button>
<button type="button" on:click={() => editor.chain().focus().unsetAllMarks().run()}>Clear marks</button>
<button type="button" on:click={() => editor.chain().focus().clearNodes().run()}>Clear nodes</button>
<button
type="button"
on:click={() => editor.chain().focus().setParagraph().run()}
class={editor.isActive("paragraph") ? "is-active" : ""}
>
P
</button>
<button
type="button"
on:click={() => editor.chain().focus().toggleHeading({ level: 1 }).run()}
class={editor.isActive("heading", { level: 1 }) ? "is-active" : ""}
>
H1
</button>
<button
type="button"
on:click={() => editor.chain().focus().toggleHeading({ level: 2 }).run()}
class={editor.isActive("heading", { level: 2 }) ? "is-active" : ""}
>
H2
</button>
<button
on:click={() => editor.chain().focus().toggleHeading({ level: 3 }).run()}
class={editor.isActive("heading", { level: 3 }) ? "is-active" : ""}
>
H3
</button>
<button
type="button"
on:click={() => editor.chain().focus().toggleHeading({ level: 4 }).run()}
class={editor.isActive("heading", { level: 4 }) ? "is-active" : ""}
>
H4
</button>
<button
type="button"
on:click={() => editor.chain().focus().toggleHeading({ level: 5 }).run()}
class={editor.isActive("heading", { level: 5 }) ? "is-active" : ""}
>
H5
</button>
<button
type="button"
on:click={() => editor.chain().focus().toggleHeading({ level: 6 }).run()}
class={editor.isActive("heading", { level: 6 }) ? "is-active" : ""}
>
H6
</button>
<button
type="button"
on:click={() => editor.chain().focus().toggleBulletList().run()}
class={editor.isActive("bulletList") ? "is-active" : ""}
>
• List
</button>
<button
type="button"
on:click={() => editor.chain().focus().toggleOrderedList().run()}
class={editor.isActive("orderedList") ? "is-active" : ""}
>
1. List
</button>
<button
type="button"
on:click={() => editor.chain().focus().toggleCodeBlock().run()}
class={editor.isActive("codeBlock") ? "is-active" : ""}
>
Code block
</button>
<button
type="button"
on:click={() => editor.chain().focus().toggleBlockquote().run()}
class={editor.isActive("blockquote") ? "is-active" : ""}
>
Quote
</button>
<button type="button" on:click={() => editor.chain().focus().setHorizontalRule().run()}>
HR
</button>
<button type="button" on:click={() => editor.chain().focus().setHardBreak().run()}>Break</button>
<button
type="button"
on:click={() => editor.chain().focus().undo().run()}
>
Undo
</button>
<button
type="button"
on:click={() => editor.chain().focus().redo().run()}
>
Redo
</button>
</div>
<div class="separator"></div>
<!-- Table controls -->
<div class="button-group">
<button type="button" on:click={() => editor.chain().focus().insertTable({ rows: 3, cols: 3, withHeaderRow: true }).run()}>
Insert table
</button>
<button type="button" on:click={() => editor.chain().focus().addColumnBefore().run()}>+ Col before</button>
<button type="button" on:click={() => editor.chain().focus().addColumnAfter().run()}>+ Col after</button>
<button type="button" on:click={() => editor.chain().focus().deleteColumn().run()}>- Col</button>
<button type="button" on:click={() => editor.chain().focus().addRowBefore().run()}>+ Row before</button>
<button type="button" on:click={() => editor.chain().focus().addRowAfter().run()}>+ Row after</button>
<button type="button" on:click={() => editor.chain().focus().deleteRow().run()}>- Row</button>
<button type="button" on:click={() => editor.chain().focus().deleteTable().run()}>Delete table</button>
<button type="button" on:click={() => editor.chain().focus().mergeCells().run()}>Merge</button>
<button type="button" on:click={() => editor.chain().focus().splitCell().run()}>Split</button>
<button type="button" on:click={() => editor.chain().focus().toggleHeaderColumn().run()}>Header col</button>
<button type="button" on:click={() => editor.chain().focus().toggleHeaderRow().run()}>Header row</button>
<button type="button" on:click={() => editor.chain().focus().toggleHeaderCell().run()}>Header cell</button>
<button type="button" on:click={() => editor.chain().focus().mergeOrSplit().run()}>Merge/Split</button>
</div>
</div>
{/if}
Customize Editor Content Style
You can style the editor content using a class or CSS selector. Tiptap does not impose any built-in styles, so you'll need to define your own.
Example CSS
<style>
.html-editor {
height: 100%;
display: flex;
flex-direction: column;
padding: 0.5rem;
}
.html-editor :global(.ProseMirror) {
height: 100%;
flex: 1;
}
.html-editor :global(.ProseMirror:focus-visible) {
outline: none;
}
.control-group {
position: sticky;
top: 0;
z-index: 10;
background: #f5f5f5;
border-bottom: 1px solid #ddd;
padding: 0.5rem;
margin-bottom: 0;
flex-shrink: 0;
}
.button-group {
display: flex;
flex-wrap: wrap;
gap: 0.25rem;
margin-bottom: 0.25rem;
}
.button-group button {
padding: 0.25rem 0.5rem;
font-size: 0.75rem;
font-weight: 500;
line-height: 1.2;
color: #333;
background-color: #fff;
border: 1px solid #ccc;
border-radius: 0.25rem;
cursor: pointer;
transition: background-color 0.15s ease;
min-width: auto;
white-space: nowrap;
}
.button-group button:hover {
background-color: #f0f0f0;
}
.button-group button:focus {
outline: 2px solid #007acc;
outline-offset: 1px;
}
.button-group button.is-active {
background-color: #333;
color: white;
border-color: #333;
}
.button-group button.is-active:hover {
background-color: #555;
border-color: #555;
}
.separator {
width: 100%;
height: 0.25rem;
}
</style>
Add Additional Extensions
By default, the Tiptap text editor doesn’t include many features such as tables, and images. To add these important features, you need to install the corresponding extensions.
Conclusion
Tiptap is a fantastic editor once it’s up and running, but integrating it with Svelte requires some workarounds and clear understanding of how Tiptap manages state and commands.
This tutorial showed:
How to initialize Tiptap in Svelte
How to pass data in and out
How to build a functional control panel
How to style the editor and controls
Hopefully, this fills in the gaps left by the official documentation and saves you the time I spent figuring it out.