Block reference
Eleven types: three containers that carry the grammar, and eight leaves that carry the content. Each one is a single entry in the definition registry.
Containers
| Type | Key styles | Notes |
|---|---|---|
body | contentWidth (560–640), backgroundColor, fontFamily, padding | The document root. Content carries subject, preheader, lang, dir and colorScheme. Defaults to a 600px content width. |
section | padding, backgroundColor, backgroundImage, borders, hideOn | A full-width band whose children stack vertically. |
frame | direction (row | stack), gap, padding, align, valign, stackOnMobile | Auto-layout. Nests up to 3 deep; a row frame holds at most 4 children, each sized by styles.size. |
Leaves
Every leaf below is legal directly inside a section or a frame (see Document & grammar). Inside a row frame, any of them may carry styles.size: { fixed: px } or { fill: pct }.
| Type | content | Notes |
|---|---|---|
heading | { runs: Run[]; level: 1 | 2 | 3 } | One heading type. There are no header1/header2/header3 variants: the level is data. |
text | { runs: Run[] } | A paragraph. Inline marks, links and variables are runs, never HTML. |
image | { src: string; alt: string; decorative?: true; href?: string } | alt may be empty only with decorative: true. Display width and height are px numbers in styles. |
button | { runs: Run[]; url: string } | Compiled as a bulletproof VML button, clickable edge to edge, not an <a> with padding. |
divider | {} | Purely visual. The rule itself is styles.borderTop. |
spacer | {} | Purely visual: styles.height. |
list | { items: Run[][]; ordered: boolean } | A boolean, not a 'ordered' | 'unordered' string. |
table | { header?: Run[][]; rows: Run[][][]; rowRepeat?: RowRepeat } | Dimensions are derived from the matrix. rowRepeat renders line items between the header and the static rows. |
Earlier drafts of the plan listed ten types and left table out. Every transactional template in the migration set is built around one, and dropping them during migration would have been precisely the silent-drop failure this project exists to eliminate. It was added through the normal expansion path: one registry entry, no grammar change.
Styles are strict, per type
Each type has its own styles vocabulary, and it is a .strict() schema rather than an open bag. A spacer has height and backgroundColor; it does not have fontSize. This is deliberate:
// ✗ fails to parse: box-shadow does not render in most email clients,
// and 'borderCollapse' is the compiler's decision, not the document's
{ type: 'table', styles: { boxShadow: '0 2px 4px rgba(0,0,0,.05)',
borderCollapse: 'collapse' } }Drift gets caught at the boundary instead of riding along to somebody’s inbox. When migrate() meets a key like that in an older document, it drops it and reports a warning, never silently.
Safety tiers
Every style key has a tier, exported as styleTiers from @repo/block-ir:
saferenders everywhere: colour, padding, borders,textAlign,lineHeight,letterSpacing,textTransform.progressivedegrades acceptably:borderRadius,darkColor/darkBackgroundColor,styles.mobileoverrides,hideOn,reverseOnMobile.restrictedloses content in some client: onlybackgroundImage, which needs a fallback colour.
The canonical schema accepts every tier; lint reports progressive keys as info and restricted keys as a warning. The AI schema omits restricted keys entirely. Dark twins and mobile overrides are derived per type, so a block can only override what it already styles. margin, maxWidth and boxShadow do not exist at all.
Defaults
Defaults come from registry[type].defaults() and nowhere else. There is no standalone getDefaultContent helper to fall out of sync with the schema, and a test builds a schema-valid block from every entry’s defaults alone.
import { createBlock, createDocument, runsFromText } from '@repo/block-ir'
createBlock('heading') // registry defaults, fresh id
createBlock('heading', { content: { runs: runsFromText('Welcome') } })
createDocument([createBlock('section')])Adding a type to this page means adding it to the registry. See Adding a block type.