← Back to list

Building Your First Rich Text Editor with ProseMirror

What is ProseMirror, and why you should care?

Rohit Gupta · 2025-07-06 18:03 · 2 claps · 5.2 min read
#rich-text-editor #javascript #prosemirror #tiptap #wysiwyg
Open on Medium ↗
Wiki topics: 🌐 · Web Development ✍️ · Writing & Creative

Building Your First Rich Text Editor with ProseMirror

What is ProseMirror, and why you should care

If you are just like me and wonder how tools like Notion or Google Docs are built and how you can also create one from scratch, ProseMirror is the obvious choice I will recommend.

ProseMirror is a powerful toolkit for building rich text editors on the web. It is just like a framework for creating interfaces inspired by what-you-see-is-what-you-get (WYSIWYG).

Unlike other WYSIWYG editors that directly edit HTML using contentEditable, ProseMirror operates on an internal data structure that is controlled through a single point of injection.

ProseMirror is a “toolkit,” not a ready-to-use editor. It provides foundational components that you assemble and extend. This allows you to

  • Load only what you need: Keeps bundle sizes small.
  • Replace components: Swap out parts of the system as needed.
  • Flexible Plugin System: Easily add custom features and package extensions.

Who is this article for?

This guide is for developers who are new to ProseMirror and want to understand its core concepts by building a simple editor from scratch.

Note: This will be a series of articles where Iwill explain ProseMirror internals and how you can customise ProseMirror to your design system. Please keep a check on the follow up articles.

Core concepts: the building blocks of ProseMirror

Since ProseMirror is a heavily customizable framework, it has few building blocks that are required to make any changes in the editor.

A small brief of these is below. Although if you want to read more about it, you can follow the official documentation.

  1. The Document (doc): Everything in the editor is a structured document, not just a BLOB of HTML. This document is the top-level node, which is a part of prosemirror-model. We will discuss more about this in the next article scheduled for this week.
  2. The State: The “single source of truth“ for the editor. It contains the document, current state, current selection, active plugin, and changes to be made.
  3. Transactions: The only way to update the state. Every change made in the editor (typing, deleting, inserting, formatting) is a transaction. This is the key to features like undo/redo.
  4. Plugins: You can add new functionality to the editor using plugins. The plugin re-renders every single time the state changes, so it always has the latest data structure for the document. Features like undo/redo or history are implemented using plugins.

Let’s build your first basic editor

Let’s build our first minimal editor using ProseMirror. By the end of this demo, you will be able to see this interface.

Step 1: Setting up your project

Set up your vanilla JavaScript project. I prefer using vite for doing that

pnpm create vite
pnpm i
pnpm dev

This will help you create and run a vanilla JS application that contains index.html, index.js, and package.json.

Now add necessary prosemirror packages to the project (e.g., prosemirror-model, prosemirror-state, prosemirror-view⁣, prosemirror-keymapand prosemirror-commands)

pnpm add prosemirror-model prosemirror-state prosemirror-keymap prosemirror-view prosemirror-commands

You can also link them up in the CDN for simplicity.

Step 2: Defining the document schema

Each prosemirror document is associated with a schema. Schema defines what kind of nodes are available in your editor. Think of it as the blueprint or grammar for your rich-text content.

We can define the styling, behavior, actions, how this looks in the actual DOM, or when which DOM element is associated with this node in schema.

A schema is made up of nodes and marks, where nodes define the structure and validations, and marks define the styling of specific HTML tags, which are associated with respective nodes.

The most basic schema contains just doc, paragraph, and text nodes. But for this demo, I have added a heading as well.

import { Schema } from "prosemirror-model";
const pDOM = ["p", 0];
const emDOM = ["em", 0];
const strongDOM = ["strong", 0];
const nodes = {
  /**
    NodeSpec The top-level document node.
    */
  doc: {
    content: "block+",
  },
  text: {
    group: "inline",
  },
  paragraph: {
    content: "inline*",
    group: "block",
    parseDOM: [{ tag: "p" }] ,
    toDOM() {
      return pDOM;
    },
  },
  heading: {
    attrs: { level: { default: 1, validate: "number" } },
    content: "inline*",
    group: "block",
    defining: true,
    parseDOM: [
      { tag: "h1", attrs: { level: 1 } },
      { tag: "h2", attrs: { level: 2 } },
      { tag: "h3", attrs: { level: 3 } },
      { tag: "h4", attrs: { level: 4 } },
      { tag: "h5", attrs: { level: 5 } },
      { tag: "h6", attrs: { level: 6 } },
    ],
    toDOM(node) {
      return ["h" + node.attrs.level, 0];
    },
  },
};
const marks = {
  em: {
    parseDOM: [
      { tag: "i" },
      { tag: "em" },
      { style: "font-style=italic" },
      { style: "font-style=normal", clearMark: (m) => m.type.name == "em"},
    ],
    toDOM() {
      return emDOM;
    },
  },
  strong: {
    parseDOM: [
      { tag: "strong" },
      {
        tag: "b",
        getAttrs: (node) => node.style.fontWeight != "normal" && null,
      },
      { style: "font-weight=400", clearMark: (m) => m.type.name == "strong"},
      {
        style: "font-weight",
        getAttrs: (value) => /^(bold(er)?|[5-9]\d{2,})$/.test(value) && null,
      },
    ],
    toDOM() {
      return strongDOM;
    },
  },
};
export const schema = new Schema({ nodes, marks });

Step 3: Creating the editor view

Editor view is the single point of injection by which we put our content in HTML.

import {schema} from "prosemirror-schema-basic"
import {EditorState} from "prosemirror-state"
import {EditorView} from "prosemirror-view"

let state = EditorState.create({schema})
// We can use document.body as well
// here instead of the #editor tag.
let view = new EditorView(document.querySelector("#editor"), {
  state,
})

Now attach this editor to a DOM element in your HTML.

<!DOCTYPE html>
<html lang="en">
    <head>
        <meta charset="UTF-8" />
        <meta name="viewport" content="width=device-width, initial-scale=1.0" />
        <title>ProseMirror Editor</title>
        <link rel="stylesheet" href="./styles.css" />
    </head>
    <body>
        <div id="editor" style="margin-bottom: 23px"></div>
        <div style="display: none" id="content">
            <h3>Welcome to My Editor</h3>
            <p>This is a rich text editor built with ProseMirror. You can click anywhere in this text and start typing to edit it.</p>
            <p>
                The editor supports various text formatting options. You can select text and apply <em>italic formatting</em>,
                <strong>bold text</strong> etc.
            </p>
        </div>
        <script type="module" src="./index.js"></script>
    </body>
</html>

Step 4: Adding basic editing plugins

In order to make editing work more interactive, we add basic plugins to the editor.

After this change, the final result index.js should look like this:

import { Schema, DOMParser } from "prosemirror-model";
import { EditorState } from "prosemirror-state";
import { EditorView } from "prosemirror-view";
import { keymap } from "prosemirror-keymap";
import { baseKeymap } from "prosemirror-commands";

import { schema } from "./schema";

const editorState = EditorState.create({
// Here we are parsing the DOM from #content and creating a new state
  doc: DOMParser.fromSchema(schema)
                .parse(document.querySelector("#content")),
  plugins: [keymap(baseKeymap)],
});

// Create and mount the editor, and pass the editor state to it
new EditorView(document.querySelector("#editor"), {
  state: editorState,
});

Conclusion and next steps

Well done! You’ve just learned the basics of ProseMirror and created a working, if basic, text editor. You’ve seen how ProseMirror’s modular architecture, which is based on states, transactions, and schemas, offers a strong basis for creating complex rich text experiences. You’ve taken the necessary initial steps, from establishing the structure of your document to giving it life with an editor view and expanding its functionality with plugins.

Next week, we’ll explore more sophisticated features like custom nodes, marks, and possibly even collaborative editing in the next part of this series.

Try adding a basic formatting option, such as bold, to your current editor as a challenge in the interim. For deeper learning, the official ProseMirror documentation and examples are the best resources. Keep experimenting, and you’ll be building powerful editors in no time!

You can find the source code mentioned in the tutorial here.


메타데이터
post_id
edbe9cbba5db
slug
building-your-first-rich-text-editor-with-prosemirror-edbe9cbba5db
url
https://medium.com/@pta.rohit28/building-your-first-rich-text-editor-with-prosemirror-edbe9cbba5db
canonical_url
https://medium.com/@pta.rohit28/building-your-first-rich-text-editor-with-prosemirror-edbe9cbba5db
author_url
https://medium.com/@pta.rohit28
status
ok
fetched_at
2026-07-19 03:09:49