AFFiNE Block-Based Editor: Core Features and Architecture Explained
AFFiNE's block-based editor provides a modular, collaborative document system with dual editing modes (Page and Edgeless), real-time Yjs synchronization, and an extensible block schema architecture.
The AFFiNE block-based editor is the core document engine powering the AFFiNE knowledge management platform. Built on the Blocksuite framework, it treats every piece of content—from paragraphs to tables to canvas shapes—as discrete, composable blocks. This architecture enables seamless switching between linear document editing and free-form whiteboarding while maintaining real-time collaboration through Yjs-based conflict resolution.
Dual Editing Modes: Page and Edgeless
The AFFiNE block-based editor exposes two primary editing surfaces through distinct web components, both extending SignalWatcher(WithDisposable(ShadowlessElement)) to enable reactive updates and lifecycle management.
PageEditor for Linear Documents
The PageEditor component in packages/frontend/core/src/blocksuite/editors/page-editor.ts renders traditional linear documents. It initializes a BlockStdScope that wires the editor instance to the underlying block store, enabling nested block hierarchies and rich text editing.
import { html, render } from 'lit';
import '@blocksuite/affine/blocksuite/editors/page-editor.js';
// Create a Yjs document (normally provided by the workspace)
const ydoc = new Y.Doc();
const store = new BlockStdScope({ store: ydoc });
// Render the editor
render(
html`<page-editor .doc=${store} .specs=${[]}></page-editor>`,
document.body
);
EdgelessEditor for Canvas Layouts
The EdgelessEditor in packages/frontend/core/src/blocksuite/editors/edgeless-editor.ts provides a canvas-style surface where blocks can be positioned absolutely. It shares the same BlockStdScope architecture as the Page editor, ensuring block compatibility across modes while enabling free-form whiteboarding capabilities.
Rich Block Library and Schema System
Every content element in the AFFiNE block-based editor is defined through a strict schema system located in blocksuite/affine/model/src/blocks/*.
Block Flavours and Models
Each block declares a flavour (unique identifier), schema, and model class. For example, the paragraph block in paragraph-model.ts defines its structure and validation rules:
// From blocksuite/affine/model/src/blocks/paragraph/paragraph-model.ts
const ParagraphBlockSchema = defineBlockSchema({
flavour: 'affine:paragraph',
props: internal => ({
text: internal.Text(),
type: 'text' as ParagraphType,
}),
metadata: {
version: 1,
role: 'content',
parent: ['affine:note', 'affine:database'],
},
});
Nested Block Relationships
Blocks declare valid parent/child relationships through the parent and children metadata fields. This enables structural validation—for example, ensuring tables can contain specific child blocks while maintaining document integrity. The table block in table-model.ts uses metadata.isFlatData: true to optimize storage for tabular data structures.
Real-Time Collaboration via Yjs
The AFFiNE block-based editor leverages Yjs for conflict-free replicated data types (CRDTs). The Workspace entity in packages/frontend/core/src/modules/workspace/entities/workspace.ts wraps a Yjs Doc and exposes LiveData streams that the UI subscribes to for reactive updates.
This architecture provides real-time multi-user synchronization, offline support, and automatic conflict resolution without manual intervention. All block operations—insertions, deletions, and updates—are transacted through the Yjs document to ensure consistency across clients.
Extensibility and Customization
The editor exposes multiple extension points for customizing behavior and UI without modifying core source code.
ViewExtension API
The AffineEditorConfigViewExtension in packages/frontend/core/src/blocksuite/view-extensions/editor-config/index.ts demonstrates how to inject custom functionality into both Page and Edgeless scopes. Extensions can register additional commands, toolbars, or UI widgets that appear automatically in both editing modes.
Slash Menu Commands
The slash menu (/) provides context-aware block insertion. Each entry is a SlashMenuConfig that defines appearance conditions, icons, and execution logic. The table insertion configuration in blocksuite/affine/blocks/table/src/configs/slash-menu.ts shows how commands chain together to insert blocks and record telemetry:
// From blocksuite/affine/blocks/table/src/configs/slash-menu.ts
std.command
.chain()
.pipe(getSelectedModelsCommand)
.pipe(insertTableBlockCommand, {
place: 'after',
removeEmptyLine: true,
})
.run();
Custom Toolbar Integration
The EditorToolbar component in blocksuite/affine/components/src/toolbar/toolbar.ts provides formatting controls and block-insertion shortcuts. Developers can extend this LitElement-based component to add custom buttons that execute editor commands.
Additional Core Capabilities
Theme and Style Integration
Both PageEditor and EdgelessEditor consume the ThemeProvider service (std.get(ThemeProvider)) to support dynamic light/dark mode switching. This ensures consistent styling across all block types and UI chrome.
Command Chain and Undo/Redo
All user actions are expressed as commands that can be chained, piped, and rolled back. The command system in @blocksuite/affine/std enables complex operations like slash-menu actions, toolbar formatting, and drag-and-drop reorganizations while maintaining a complete undo history.
Summary
- Dual editing modes: The AFFiNE block-based editor provides both linear
PageEditorand canvas-styleEdgelessEditorcomponents, sharing a commonBlockStdScopearchitecture. - Schema-driven blocks: Every content element is a typed block with defined flavours, parent/child relationships, and metadata (e.g.,
isFlatDatafor tables) located inblocksuite/affine/model/src/blocks/*. - Real-time collaboration: Yjs-backed
Workspaceentities provide CRDT-based synchronization throughLiveDatastreams inpackages/frontend/core/src/modules/workspace/entities/workspace.ts. - Extensible architecture: ViewExtensions, slash-menu configurations, and toolbar components allow custom functionality injection without core modifications.
- Command-based operations: All edits use chainable commands with full undo/redo support via the
@blocksuite/affine/stdsystem.
Frequently Asked Questions
What block types does the AFFiNE block-based editor support?
AFFiNE supports a comprehensive library of block types defined in blocksuite/affine/model/src/blocks/*, including paragraphs, headings, numbered and bulleted lists, tables, code blocks, images, embeds, and callouts. Each block declares a unique flavour (e.g., affine:paragraph, affine:table) and schema metadata that defines valid parent/child relationships, enabling complex nested structures while maintaining document integrity.
How does AFFiNE handle real-time collaboration?
The editor leverages Yjs (Y-JavaScript) for conflict-free replicated data types (CRDTs). The Workspace entity in packages/frontend/core/src/modules/workspace/entities/workspace.ts wraps a Yjs Doc and exposes LiveData reactive streams that the UI subscribes to. This architecture enables real-time multi-user synchronization, offline editing with automatic reconnection, and conflict resolution without manual merge intervention, as all block operations are transacted through the Yjs document.
Can I extend AFFiNE's editor with custom blocks or UI components?
Yes, the AFFiNE block-based editor provides multiple extension points. You can create custom ViewExtensions (as demonstrated in packages/frontend/core/src/blocksuite/view-extensions/editor-config/index.ts) to inject commands, toolbar buttons, or UI widgets into both Page and Edgeless editors. Additionally, you can define custom SlashMenuConfig entries to add quick-insert commands, or extend the EditorToolbar component in blocksuite/affine/components/src/toolbar/toolbar.ts to add formatting controls that execute custom command chains.
What is the difference between Page and Edgeless editing modes?
Page mode (implemented in packages/frontend/core/src/blocksuite/editors/page-editor.ts) provides a traditional linear document experience where blocks flow vertically in a reading order, similar to conventional word processors. Edgeless mode (in packages/frontend/core/src/blocksuite/editors/edgeless-editor.ts) offers a canvas-style whiteboard where blocks can be positioned absolutely anywhere, supporting free-form diagrams and spatial organization. Both modes share the same underlying BlockStdScope architecture and block definitions, ensuring content compatibility when switching between views.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →