Problem
Our editor forces all content into a single vertical stack. Every block — paragraphs, headings, images, code — sits directly above or below the next one. There is no way for users to place content side by side.
This is a significant limitation. Users regularly need multi-column layouts for:
Comparisons: two approaches, before/after, pros/cons
Mixed media: text alongside an image or embed
Structured sections: splitting a page into logically distinct areas
Information density: making better use of wide screens instead of leaving margins empty
Competing editors (Notion, Google Docs, Coda) all support columns or similar horizontal layout. Users who come from these tools expect it. Without columns, users resort to workarounds like embedding tables (that de do not support either) or creating images, both of which break the editing experience.
Solution
Documents are trees of BlockNode { block, children }. Blocks stack vertically — no way to place blocks side-by-side. We want two layout features: Fixed Columns (explicit column containers) and Flow Grid (items wrap into N columns). This document analyzes 3 solutions and recommends one.
The Deciding Constraint: ProseMirror Node Hierarchy
doc → blockChildren → blockNode+ → (block, blockChildren?)block nodes can ONLY contain inline* or '' — cannot contain other blocks
blockChildren is the only nesting mechanism, has a listType attribute
All editor commands (Enter, Backspace, Tab, split, merge) assume this exact hierarchy
New block types created via createReactBlockSpec are block group nodes — same constraint
This means dedicated column node types outside the hierarchy (Solution B2) require refactoring 15-20+ editor files. And flat attribute markers (Solution C) fight ProseMirror's model entirely.
Three Solutions Evaluated
Solution A: childrenType: 'Columns' — RECOMMENDED
Add 'Columns' to HMBlockChildrenType. Renders children as side-by-side columns.
blockNode (container, childrenType: "Columns")
block:paragraph ("") ← empty, invisible
blockChildren [listType='Columns'] ← render as flex row
blockNode (column 1)
block:paragraph ("") ← empty, invisible
blockChildren [listType='Group'] ← column 1 content
blockNode → paragraph ("Left text")
blockNode → image (...)
blockNode (column 2)
block:paragraph ("")
blockChildren [listType='Group'] ← column 2 content
blockNode → paragraph ("Right text")
DimensionRatingPM schema fitPerfect — no new node typesCRDT safetyHigh — each column is an independent RGA sublistEditor effortModerate — keyboard guards on ~6-8 handlersBackwards compatModerate — old clients stack blocks vertically
Solution B: Dedicated Columns + Column Block Types
Two new block types with explicit semantics (like Notion's column_list/column).
B1 (within existing hierarchy): Functionally identical to A in ProseMirror — the dedicated type is just a semantic label on the block content node. Same effort as A.
B2 (custom PM nodes): Requires refactoring getBlockInfoFromPos, getGroupInfoFromPos, nodeToBlock/blockToNode, all keyboard shortcuts, normalizeFragment, splitBlock/mergeBlocks/nestBlock/unnestBlock, side menu detection. 15-20+ files, high risk.
Old clients show "Unsupported Block" for the column wrappers.
Solution C: Flat Attribute Markers (columnGroupId, columnIndex)
No structural changes — sibling blocks get column attributes, rendering groups them visually.
CRDT fragile: concurrent edits interleave blocks, break group contiguity
Editor: requires complex ProseMirror decoration plugin, fights the document model
Best backwards compat but worst reliability
Comparison
A: childrenTypeB1: Dedicated (same PM)B2: Dedicated (new PM)C: FlatPM schema changesNoneNoneMajor (2 nodes)NoneEditor effort~6-8 guardsSame as A15-20+ filesPlugin rewriteCRDT safetyHighHighHighLowBackwards compatModerateModeratePoorBestRiskLowLowHighVery High
Recommendation: Solution A
Solution A wins across all dimensions. Same tree shape as Notion's model, expressed within our existing ProseMirror hierarchy. No new node types, no hierarchy changes. Can optionally layer B1 semantic naming on top later.
Two Layout Features (Both Wanted)
1. Fixed Columns (childrenType: 'Columns')
Each child IS a column container with its own content blocks
Column count = number of children (users add/remove explicitly)
Optional columnWidths attribute for proportions
Use case: side-by-side content sections
2. Flow Grid (childrenType: 'Grid')
Items wrap into N columns automatically (like Query block card view)
columnCount attribute defines max columns, items stack/wrap
Single flat list of children, CSS grid handles flow
Use case: card grids, gallery layouts
Both use the same pattern: new childrenType values + CSS layout in BlockNodeList.
Mobile view concerns
For the Column layout we have two options:
keep columns and add a horizontal scroll so readers can see all columns
stack columns vertically so users don't have to scroll horizontally to see all the content.
On the other hand, for Grid Layout, we will always stack items one of top of the other, and we will do exactly what happens with the grid view in query blocks.
For columns, I prefer to just stack them vertically, I don't think its a bad expectation for readers. adding an extra horizontal scroll adds a bit more complexity to the layout that I prefer not to add.
Notion Reference
Notion uses column_list + column (Solution B). Rules: min 2 columns, min 1 child per column, width_ratio (0-1, sum to 1), no nested columns. They can do this because they built their editor from scratch — no ProseMirror constraint.
Photo by Jesse Bauer on Unsplash
Grid Layout — Implementation Plan