Skip to main content
In this tutorial, you’ll build a ProseKit editor from the ground up, learning the core concepts along the way. By the end, you’ll understand how extensions work, how to compose them, and how to customize your editor.

What You’ll Build

You’ll create a rich text editor with:
  • Basic text editing
  • Bold and italic formatting
  • Headings
  • Keyboard shortcuts
  • A simple toolbar
This tutorial focuses on understanding the fundamentals. For a production-ready editor, see the Quick Start guide.

Prerequisites

Before starting, make sure you have:
  • ProseKit installed in your project (Installation guide)
  • Basic knowledge of your chosen framework (React, Vue, etc.)
  • A text editor and development server running

Step 1: Create a Minimal Editor

Let’s start with the absolute minimum to get an editor working.
1
Create the component file
2
Create a new file for your editor component (e.g., Editor.tsx for React or Editor.vue for Vue).
3
Import the required modules
4
At the top of your file, import the core ProseKit modules:
5
React
import 'prosekit/basic/style.css'

import { createEditor, union } from 'prosekit/core'
import { ProseKit } from 'prosekit/react'
import { useMemo } from 'react'
Vue
<script setup lang="ts">
import 'prosekit/basic/style.css'

import { createEditor, union } from 'prosekit/core'
import { ProseKit } from 'prosekit/vue'
</script>
6
We’re importing prosekit/basic/style.css which contains essential styles. Without this, the editor won’t work correctly.
7
Define your first extension
8
Every ProseKit editor needs at least two node types: doc (the document root) and text (for text content):
9
React
import { defineDoc } from 'prosekit/extensions/doc'
import { defineText } from 'prosekit/extensions/text'
import { defineParagraph } from 'prosekit/extensions/paragraph'

function defineExtension() {
  return union(
    defineDoc(),
    defineText(),
    defineParagraph(),
  )
}
Vue
import { defineDoc } from 'prosekit/extensions/doc'
import { defineText } from 'prosekit/extensions/text'
import { defineParagraph } from 'prosekit/extensions/paragraph'

function defineExtension() {
  return union(
    defineDoc(),
    defineText(),
    defineParagraph(),
  )
}
</script>
10
The union function combines multiple extensions into one. Think of it like composing LEGO blocks - each extension adds specific functionality.
11
Create and mount the editor
12
Now create the editor and mount it to a DOM element:
13
React
export default function Editor() {
  const editor = useMemo(() => {
    const extension = defineExtension()
    return createEditor({ extension })
  }, [])

  return (
    <ProseKit editor={editor}>
      <div 
        ref={editor.mount} 
        className="editor"
        style={{ 
          minHeight: '200px', 
          padding: '16px',
          border: '1px solid #ccc',
          borderRadius: '4px'
        }}
      />
    </ProseKit>
  )
}
Vue
<script setup lang="ts">
// ... previous imports ...

const extension = defineExtension()
const editor = createEditor({ extension })
</script>

<template>
  <ProseKit :editor="editor">
    <div 
      :ref="(el) => editor.mount(el as HTMLElement | null)" 
      class="editor"
      style="
        min-height: 200px;
        padding: 16px;
        border: 1px solid #ccc;
        border-radius: 4px;
      "
    />
  </ProseKit>
</template>
Congratulations! You now have a working (albeit basic) text editor. You can type in it, but it doesn’t have formatting yet.

Step 2: Add Text Formatting

Let’s add bold and italic formatting capabilities.
1
Import the formatting extensions
2
React
import { defineBold } from 'prosekit/extensions/bold'
import { defineItalic } from 'prosekit/extensions/italic'
Vue
import { defineBold } from 'prosekit/extensions/bold'
import { defineItalic } from 'prosekit/extensions/italic'
3
Update your extension definition
4
Add the formatting extensions to your union call:
5
function defineExtension() {
  return union(
    // Nodes
    defineDoc(),
    defineText(),
    defineParagraph(),
    // Marks (formatting)
    defineBold(),
    defineItalic(),
  )
}
Now your editor supports bold and italic! Each extension (defineBold() and defineItalic()) includes:
  • The mark specification (how it’s stored and rendered)
  • Commands to toggle the formatting
  • Keyboard shortcuts (Cmd+B for bold, Cmd+I for italic)
  • Input rules (e.g., **text** becomes bold)
Try typing **hello** and the text will automatically become bold!

Step 3: Add Headings

Let’s add support for headings (h1, h2, h3, etc.).
1
Import the heading extension
2
import { defineHeading } from 'prosekit/extensions/heading'
3
Add it to your extension
4
function defineExtension() {
  return union(
    defineDoc(),
    defineText(),
    defineParagraph(),
    defineHeading(),  // Add this
    defineBold(),
    defineItalic(),
  )
}
Now you can create headings by typing # followed by a space for h1, ## for h2, and so on!

Step 4: Add Essential Features

A useful editor needs undo/redo, keyboard shortcuts, and history. Let’s add these:
1
Import the core features
2
import { 
  defineBaseKeymap, 
  defineBaseCommands, 
  defineHistory 
} from 'prosekit/core'
3
Add them to your extension
4
function defineExtension() {
  return union(
    // Nodes
    defineDoc(),
    defineText(),
    defineParagraph(),
    defineHeading(),
    // Marks
    defineBold(),
    defineItalic(),
    // Core features
    defineBaseKeymap(),    // Essential keyboard shortcuts
    defineBaseCommands(),  // Basic commands
    defineHistory(),       // Undo/redo support
  )
}
Now you can:
  • Press Cmd+Z to undo (Ctrl+Z on Windows)
  • Press Cmd+Shift+Z to redo
  • Use standard text editing shortcuts (arrows, backspace, etc.)

Step 5: Add a Simple Toolbar

Let’s create a toolbar with buttons for bold, italic, and headings.
1
Create a toolbar component
2
React
import { useEditor } from 'prosekit/react'

function Toolbar() {
  const editor = useEditor()

  const toggleBold = () => {
    editor.commands.toggleBold()
    editor.focus()
  }

  const toggleItalic = () => {
    editor.commands.toggleItalic()
    editor.focus()
  }

  const setHeading = (level: 1 | 2 | 3) => {
    editor.commands.setHeading({ level })
    editor.focus()
  }

  const setParagraph = () => {
    editor.commands.setParagraph()
    editor.focus()
  }

  return (
    <div style={{ 
      padding: '8px', 
      borderBottom: '1px solid #ccc',
      display: 'flex',
      gap: '8px'
    }}>
      <button onClick={toggleBold} style={{ fontWeight: 'bold' }}>
        B
      </button>
      <button onClick={toggleItalic} style={{ fontStyle: 'italic' }}>
        I
      </button>
      <button onClick={() => setHeading(1)}>H1</button>
      <button onClick={() => setHeading(2)}>H2</button>
      <button onClick={() => setHeading(3)}>H3</button>
      <button onClick={setParagraph}>P</button>
    </div>
  )
}
Vue
<script setup lang="ts">
import { useEditor } from 'prosekit/vue'

const editor = useEditor()

const toggleBold = () => {
  editor.value.commands.toggleBold()
  editor.value.focus()
}

const toggleItalic = () => {
  editor.value.commands.toggleItalic()
  editor.value.focus()
}

const setHeading = (level: 1 | 2 | 3) => {
  editor.value.commands.setHeading({ level })
  editor.value.focus()
}

const setParagraph = () => {
  editor.value.commands.setParagraph()
  editor.value.focus()
}
</script>

<template>
  <div :style="{ 
    padding: '8px', 
    borderBottom: '1px solid #ccc',
    display: 'flex',
    gap: '8px'
  }">
    <button @click="toggleBold" :style="{ fontWeight: 'bold' }">
      B
    </button>
    <button @click="toggleItalic" :style="{ fontStyle: 'italic' }">
      I
    </button>
    <button @click="() => setHeading(1)">H1</button>
    <button @click="() => setHeading(2)">H2</button>
    <button @click="() => setHeading(3)">H3</button>
    <button @click="setParagraph">P</button>
  </div>
</template>
3
Add the toolbar to your editor
4
React
export default function Editor() {
  const editor = useMemo(() => {
    const extension = defineExtension()
    return createEditor({ extension })
  }, [])

  return (
    <ProseKit editor={editor}>
      <div style={{ border: '1px solid #ccc', borderRadius: '4px' }}>
        <Toolbar />
        <div 
          ref={editor.mount} 
          style={{ 
            minHeight: '200px', 
            padding: '16px'
          }}
        />
      </div>
    </ProseKit>
  )
}
Vue
<template>
  <ProseKit :editor="editor">
    <div :style="{ border: '1px solid #ccc', borderRadius: '4px' }">
      <Toolbar />
      <div 
        :ref="(el) => editor.mount(el as HTMLElement | null)" 
        :style="{ 
          minHeight: '200px', 
          padding: '16px'
        }"
      />
    </div>
  </ProseKit>
</template>
Now you have a toolbar with buttons to format your text!

Step 6: Add Typography Styles

Let’s make the editor content look better with ProseKit’s typography styles. Add this import at the top of your file:
import 'prosekit/basic/typography.css'
This adds beautiful default styles for headings, paragraphs, and other elements.

Complete Code

Here’s your complete editor with all the features we’ve added:
import 'prosekit/basic/style.css'
import 'prosekit/basic/typography.css'

import { createEditor, union, defineBaseKeymap, defineBaseCommands, defineHistory } from 'prosekit/core'
import { defineDoc } from 'prosekit/extensions/doc'
import { defineText } from 'prosekit/extensions/text'
import { defineParagraph } from 'prosekit/extensions/paragraph'
import { defineBold } from 'prosekit/extensions/bold'
import { defineItalic } from 'prosekit/extensions/italic'
import { defineHeading } from 'prosekit/extensions/heading'
import { ProseKit, useEditor } from 'prosekit/react'
import { useMemo } from 'react'

function defineExtension() {
  return union(
    defineDoc(),
    defineText(),
    defineParagraph(),
    defineHeading(),
    defineBold(),
    defineItalic(),
    defineBaseKeymap(),
    defineBaseCommands(),
    defineHistory(),
  )
}

function Toolbar() {
  const editor = useEditor()

  return (
    <div style={{ 
      padding: '8px', 
      borderBottom: '1px solid #e5e7eb',
      display: 'flex',
      gap: '8px',
      flexWrap: 'wrap'
    }}>
      <button 
        onClick={() => {
          editor.commands.toggleBold()
          editor.focus()
        }}
        style={{ 
          fontWeight: 'bold',
          padding: '4px 12px',
          border: '1px solid #d1d5db',
          borderRadius: '4px',
          background: 'white',
          cursor: 'pointer'
        }}
      >
        B
      </button>
      <button 
        onClick={() => {
          editor.commands.toggleItalic()
          editor.focus()
        }}
        style={{ 
          fontStyle: 'italic',
          padding: '4px 12px',
          border: '1px solid #d1d5db',
          borderRadius: '4px',
          background: 'white',
          cursor: 'pointer'
        }}
      >
        I
      </button>
      <button onClick={() => {
        editor.commands.setHeading({ level: 1 })
        editor.focus()
      }} style={{ padding: '4px 12px', border: '1px solid #d1d5db', borderRadius: '4px', background: 'white', cursor: 'pointer' }}>
        H1
      </button>
      <button onClick={() => {
        editor.commands.setHeading({ level: 2 })
        editor.focus()
      }} style={{ padding: '4px 12px', border: '1px solid #d1d5db', borderRadius: '4px', background: 'white', cursor: 'pointer' }}>
        H2
      </button>
      <button onClick={() => {
        editor.commands.setHeading({ level: 3 })
        editor.focus()
      }} style={{ padding: '4px 12px', border: '1px solid #d1d5db', borderRadius: '4px', background: 'white', cursor: 'pointer' }}>
        H3
      </button>
      <button onClick={() => {
        editor.commands.setParagraph()
        editor.focus()
      }} style={{ padding: '4px 12px', border: '1px solid #d1d5db', borderRadius: '4px', background: 'white', cursor: 'pointer' }}>
        P
      </button>
    </div>
  )
}

export default function Editor() {
  const editor = useMemo(() => {
    const extension = defineExtension()
    return createEditor({ extension })
  }, [])

  return (
    <ProseKit editor={editor}>
      <div style={{ 
        border: '1px solid #e5e7eb', 
        borderRadius: '8px',
        overflow: 'hidden'
      }}>
        <Toolbar />
        <div 
          ref={editor.mount} 
          style={{ 
            minHeight: '200px', 
            padding: '16px',
            outline: 'none'
          }}
        />
      </div>
    </ProseKit>
  )
}

What You’ve Learned

In this tutorial, you learned:
  1. Extensions are composable: Use union() to combine multiple extensions
  2. Node types: Every editor needs at least doc, text, and a block node like paragraph
  3. Marks: Formatting like bold and italic are called “marks”
  4. Commands: Each extension provides commands to modify the document
  5. Hooks: Use framework-specific hooks like useEditor() to access the editor instance
  6. The ProseKit component: Provides context for all child components

Using defineBasicExtension

In this tutorial, we built an extension from scratch to understand the fundamentals. In practice, you’ll often use defineBasicExtension() which includes all of these features and more:
import { defineBasicExtension } from 'prosekit/basic'

const extension = defineBasicExtension()
This gives you:
  • Everything we built above
  • Plus: links, lists, blockquotes, code blocks, tables, images, and more
  • Plus: input rules, keyboard shortcuts, and automatic transformations

Next Steps

Now that you understand the basics, explore these topics:

Extensions Guide

Learn about all available extensions and how to customize them

Styling

Make your editor beautiful with custom styles

Saving Content

Learn how to save and load editor content

API Reference

Explore the complete API documentation
For a production-ready editor with a full toolbar, inline menu, and more, check out the full example or install it with shadcn/ui.