Content blocks
Block model
Every piece of a page is a block. `@krackedui/motion` models a page as a `Block[]` tree and renders it with consistent block spacing; the same tree drives the read-only renderer and the editor.
Simple things should be simple.
The Block type
A block has an id, a type, optional rich text, optional nested children, an optional color and type-specific props. Nesting is real nesting: list items, toggles, quotes and callouts hold their sub-blocks in children; columns are a column_list whose children are column blocks.
Which props apply depends on the type:
heading_*:toggleable,opentoggle:open·to_do:checkedcallout:icon(emoji)code:language(the code itself istext)equation:expression(TeX)image:src,caption,widthbookmark:url,title,description,thumbnail,favicontable:rows(RichText cells),hasHeaderRow,hasHeaderColumn,columnWidthscolumn:ratio(0..1 width share) ·page:icon(the title istext)
Rich text
RichText is a list of runs. Each run has text, optional marks (bold, italic, underline, strike, code, equation, link, color) and optionally a mention ({ kind: "page" | "user" | "date", label, icon? }) that renders as a non-editable token.
Colors
BlockColor is "default", one of nine color names (gray, brown, orange, yellow, green, blue, purple, pink, red) for colored text, or <name>_background for a tinted background. The same values work on a block (block.color) and on a run of text (marks.color). Text colors use <c>-text-secondary, backgrounds <c>-bg-secondary (callouts use the lighter <c>-bg-primary).
Helpers
rt(text, marks?)builds rich text from a string (rt("")is[]).plainText(richText)joins the runs back into a string.blockId()returns a short unique id for new blocks.blockTypeNamesmaps each type to its display label ("To-do list", "Toggle list"…);isListLike,isHeading,textBlockTypesclassify types.collectHeadings(blocks)returns the headings used by the table of contents.newBlock(kind)and theblockTreehelpers (findBlock,insertIn,removeIn,indentIn…) come from the editor entry.
BlockContent
Block sizes are relative to the 16px / 1.5 content type (headings, code and inline code use em). BlockRenderer and Editor set it for you; when you render individual blocks on their own, wrap them in BlockContent, otherwise they inherit whatever font size surrounds them.
Usage
- Use
BlockRendererto show storedBlock[]content read-only (published pages, previews, help articles). - Use
Editorwhen people write or rearrange the content; both share the sameBlock[]model. - Wrap individual blocks in
BlockContentwhen you render them outsideBlockRendererorEditor, so they get the 16px content type. - Don’t use blocks for app chrome (forms, toolbars, settings); use the UI components instead.
Installation
Install the package (React 19):
Import the styles once, in your app entry or root layout:
Use it anywhere inside your app's <Theme> (see Installation):
API Reference
BlockRenderer
Renders a Block[] tree read-only with the block rhythm, list markers and numbering.
| Prop | Type | Default |
|---|---|---|
blocks | Block[] | |
renderLeaf | (block: Block, p: LeafRenderProps) => ReactNode | |
wrapBlock | (block: Block, node: ReactElement, ctx: BlockRenderContext) => ReactNode | |
onBlockChange | (id: string, props: Partial<BlockProps>) => void | |
renderCell | (block: Block, row: number, col: number, className: string) => ReactNode | |
renderEquationEditor | (block: Block) => ReactNode | |
onEquationEdit | (block: Block) => void | |
onEmptyToggleClick | (block: Block) => void | |
className | string |
BlockContent
Content container for standalone blocks: flex flex-col, 16px / 1.5.
| Prop | Type | Default |
|---|---|---|
children | ReactNode | |
className | string |
TextMarks
Inline formatting of one run.
| Prop | Type | Default |
|---|---|---|
bold | boolean | |
italic | boolean | |
underline | boolean | |
strike | boolean | |
code | boolean | |
equation | boolean | |
link | string | |
color | BlockColor |
Mention
| Prop | Type | Default |
|---|---|---|
kind | "page""user""date" | |
label | string | |
icon | string | "📄" |
Accessibility
- The renderer outputs plain flowing content: headings are visual (styled
divs); give the page a realh1(see Page title) if you need a document outline. - Toggles and to-dos stay interactive in read-only mode: they toggle locally unless you handle
onBlockChange. - Each rendered block carries
data-block-id, which the table of contents uses to scroll to headings.