Skip to content
Greta.Agency
Case Studies

How Notion Built a Flexible Editor (And How You Can Too)

Notion's block-based editor handles text, databases, embeds, and code — all in one composable system. Here's the architecture and how to build your own version.

MichaelApril 7, 20264 min read

Feature Overview

Notion's editor is a block-based document system. Every piece of content — a paragraph, a heading, a to-do item, a database, an image — is a "block" with a type and properties. Blocks can be nested, reordered, transformed into other block types, and synced in real time across multiple users.

This architecture is what makes Notion flexible enough to be a doc tool, a project tracker, a wiki, and a database — all in one product.

The Block Data Model

Every Notion page is a tree of blocks:

interface Block {
  id: string;
  type: 'paragraph' | 'heading_1' | 'heading_2' | 'bulleted_list' | 
        'numbered_list' | 'todo' | 'toggle' | 'code' | 'image' | 
        'database' | 'callout' | 'divider';
  properties: Record<string, any>; // type-specific content
  children: string[]; // ordered list of child block IDs
  parent_id: string | null;
  created_at: string;
  updated_at: string;
}

// Example paragraph block
{
  id: "abc123",
  type: "paragraph",
  properties: {
    rich_text: [
      { type: "text", text: "Hello ", bold: false },
      { type: "text", text: "world", bold: true }
    ]
  },
  children: [],
  parent_id: "page_xyz"
}

The rich_text array within a block handles inline formatting — bold, italic, code, links — without a separate node type for each. This keeps the block tree flat while supporting rich inline content.

Tech Approach

Option 1: Build on BlockNote (Recommended)

BlockNote is an open-source Notion-like editor built on TipTap and ProseMirror. It handles:

  • Block types (paragraph, heading, list, code, image)
  • Slash commands (/ menu to insert block types)
  • Drag-and-drop reordering
  • Markdown shortcuts
import { BlockNoteEditor } from "@blocknote/core";
import { BlockNoteView, useBlockNote } from "@blocknote/react";

function Editor() {
  const editor = useBlockNote({
    onEditorContentChange: (editor) => {
      const blocks = editor.topLevelBlocks;
      saveToDatabase(blocks);
    }
  });
  
  return <BlockNoteView editor={editor} />;
}

Building on BlockNote gives you 80% of Notion's editor in 1–2 days.

Option 2: Build on TipTap directly

TipTap is lower-level than BlockNote — you define your own node types, extensions, and commands. More flexibility, more work.

Use TipTap when you need custom block types that BlockNote doesn't support (like Notion's databases, or specialized chart blocks).

Option 3: Build from scratch with ProseMirror

Only if you have specific requirements that existing libraries can't meet. ProseMirror is the foundation under TipTap — extremely powerful, extremely complex. 8–12 weeks minimum to build a production editor.

Real-Time Collaboration

Adding collaborative editing (multiple cursors, conflict-free concurrent edits) requires a CRDT (Conflict-free Replicated Data Type) — specifically Yjs, which both BlockNote and TipTap support natively.

import * as Y from 'yjs';
import { WebsocketProvider } from 'y-websocket';

const ydoc = new Y.Doc();
const provider = new WebsocketProvider(
  'wss://your-yjs-server.com',
  'document-room-id',
  ydoc
);

// BlockNote integration
const editor = useBlockNote({
  collaboration: {
    provider,
    fragment: ydoc.getXmlFragment("document-store"),
    user: { name: "Alice", color: "#7CFC00" }
  }
});

The Yjs provider handles operational transforms — when two users edit the same paragraph simultaneously, both edits are merged correctly without conflicts or lost data.

Slash Commands

The / menu is the UX pattern that makes block editors discoverable. Implementation:

// BlockNote handles this natively
// For TipTap, use the @tiptap/suggestion extension
const slashCommands = [
  { title: "Heading 1", command: ({ editor }) => editor.chain().toggleHeading({ level: 1 }).run() },
  { title: "To-do list", command: ({ editor }) => editor.chain().toggleTaskList().run() },
  { title: "Code block", command: ({ editor }) => editor.chain().toggleCodeBlock().run() },
];

Simplified Steps to Ship

Week 1:

  • Set up BlockNote with your tech stack
  • Implement save/load from your database
  • Add the core block types your use case needs

Week 2:

  • Add real-time collaboration with Yjs (if needed)
  • Implement presence indicators (user avatars on shared docs)
  • Add image upload (Supabase Storage or Cloudinary)

Week 3:

  • Custom block types specific to your product
  • Export functionality (Markdown, PDF)
  • Version history (save snapshots on each edit, allow revert)

Estimated Complexity

| Feature | Complexity | Time | |---|---|---| | Basic rich text editor (BlockNote) | Low | 1–2 days | | Custom block types | Medium | 3–5 days | | Real-time collaboration | High | 1 week | | Full Notion-like system with databases | Very High | 6–10 weeks |

M

Written by

Michael

Lead Engineer, Greta Agency

Michael has built rich text editors, block-based content systems, and collaborative document tools.