Rendering

Layout and spacing

How lttr decides the space above and below every module, from class defaults, pair rules and position rules.

You never set spacing in a template. Modules do not look at their neighbours, and there is no margin or padding prop. All vertical space between modules is decided in one place, layout.Arrange, from rules taken from the Quiet design's reference frames. The same template always gets the same spacing, and a new combination of modules still looks deliberate.

package layout
func Arrange(mods []ir.Module, header HeaderVariant) []Section

type Section struct {
    Module            ir.Module
    PadTop, PadBottom int // px
}

Arrange runs three passes over the template's modules: class defaults, then pair rules, then position rules. The HTML renderer writes each section as a table row, <td class="px" style="padding:{top}px 56px {bottom}px 56px;text-align:…">, so a section's padding is the space it adds above and below itself. Bands (images and maps) are full-bleed and ignore the side padding.

Know the module classes

Every module belongs to one layout class:

ClassModules
Herostatement, figure, offer
Bandimage, map
Tabularreceipt, breakdown, compare, table, stock
Bodyletter, meta, quote
Noticenotice
Tailprogress, code, actions
Editorialstories, products
Signoffsignature, signoff
Ruledivider

Start from the class defaults

ClassTop / bottom (px)
Hero40 / 36
Band0 / 0
Tabular32 / 32
Body (letter)32 / 28
Body (meta)0 / 32
Body (quote)40 / 40
Notice0 / 32
Tail (progress)0 / 8
Tail (code, actions)0 / 40
Editorial0 / 0 (the tiles carry their own 32 / 36 padding)
Signoff0 / 36
Rule0 / 0

The actions default is 40, not SPEC §8's 36: the handoff's Q2 frame wins on visual values (D10).

Apply the pair rules

Pair rules look at two adjacent modules and override the bottom of the first, the top of the second, or both. They are data ([]layout.Rule, in layout/rules.go), each citing the reference frame its value comes from. They apply in order, and a later match overrides an earlier one on the same side.

Previous → nextEffectSource
Hero → Tail (progress)Hero bottom 28Q1
Hero (statement) → Tabular (compare)Hero bottom 24Q7
Hero (statement) → Tabular (table)Hero bottom 16Q9 (D10: SPEC says 24)
Hero (statement) → Tabular (receipt)Hero bottom 24SPEC §8
Hero (figure) → TabularHero bottom 32, Tabular top 0Q2 (D10)
Hero → Tail (code)Hero bottom 0Q3
Body (meta) → TabularTabular top 0Q6
Band → Body (meta)Meta top 32Q6
Tabular → Tail (actions)Actions top 8Q2

A statement followed by breakdown or stock matches no rule and keeps the Hero default of 36.

Apply the position rules

Last, a few rules look at where a module sits in the whole email:

PositionEffectSource
A statement that is the first module under an H3 headerBottom 32Q4
A Hero that is the last moduleBottom 40Q5 (D10)
A notice that is the last module, or follows a BandBottom 40SPEC §8
A letter that is the first moduleTop 36SPEC §8

Check the nine reference stacks

The nine Quiet examples are the conformance suite for these rules. layout/arrange_test.go asserts every value below.

ExampleHeaderSections (top / bottom)
01 orderH1statement 40/28 · progress 0/8 · receipt 32/32 · meta 0/32
02 payoutH1figure 40/32 · breakdown 0/32 · actions 8/40 · signoff 0/36
03 resetH1statement 40/0 · code 0/40 · notice 0/40
04 journalH3statement 40/32 · image 0/0 · stories 0/0 · divider 0/0 · quote 40/40
05 campaignH1statement 40/36 · image 0/0 · products 0/0 · divider 0/0 · offer 40/40
06 job offerH1statement 40/36 · map 0/0 · meta 32/32 · breakdown 0/32
07 fee changeH1statement 40/24 · compare 32/32 · divider 0/0 · letter 32/28 · signoff 0/36
08 outreachH1letter 36/28 · signature 0/36
09 internalH2statement 40/16 · table 32/32 · notice 0/32 · signoff 0/36

Here is 02. The figure → breakdown rule gives the figure a 32px bottom and the breakdown no top padding, and the Tabular → actions rule drops the actions' top padding to 8:

The email gallery is not available
public/gallery/manifest.json was not found when the site was built. Run `pnpm gallery` in docs-site/ (it needs Go).

Change the spacing

The rules are part of the library, not the theme, so a consumer cannot change them per brand. If a combination of modules looks wrong, the fix is a new pair rule in layout/rules.go with its source, plus a case in layout/arrange_test.go, and a golden update (see Goldens and testing).

Copyright © 2026