Layout

layout applies only to containers (content.type: "container"). It positions that node's children. Leaves (text, image, table, …) do not carry layout.

Omit layout or use a vertical stack for ordinary top-to-bottom flow. Sibling paragraphs are separate text nodes; space between them is the parent layout.gap (see Text).

Copy ex_stack.json and ex_grid.json into content/root.json children. Keep id: "root" — do not replace root.json with a catalog fragment.

Root rule

content/root.json must omit layout or use a vertical stack only. A grid, horizontal stack, overlay, or columns layout belongs on a nested child (for example rootmain.section → grid).

Stack

layout.type: "stack", or omit type (stack is the default).

FieldMeaning
directionvertical (default) or horizontal
gapSpace between children, millipt (1000 = 1 pt)
align_itemsCross-axis: start, center, end, stretch
justify_contentMain-axis: start, center, end — not CSS space-between
width / heightOptional fixed outer size, millipt

Title on the left and logo, meta, or amount on the right is a two-column grid, not a flex row: ex_split_bar.json. A block pinned to the trailing edge with left-aligned lines uses ex_end_block.json.

Grid

layout.type: "grid" with required columns and optional rows.

Tracks are always objects — never bare integers:

TrackMeaning
{ "fr": N }Share leftover space along that axis
{ "pt": N }Fixed size, millipt
{ "auto": true }Size from the largest child in that track
GoalApproach
N equal columnsN { "fr": 1 } column tracks
Auto-wrapped rowsOmit rows — the engine adds { "auto": true } rows (ceil(children ÷ columns))
Fixed row templateSet rows explicitly — the list does not grow when you add children
fr rowsThe grid needs a finite outer height (layout.height or a fixed-height parent)
fr columnsThe grid needs a finite outer width (or a bounded parent)

Optional: gap, row_gap, column_gap, cell_align (default stretch), width, height. Working example: ex_grid.json.

Page size and spacing

All layout numbers are millipt (1 pt = 1000). Page width, height, and margins are in manifest.json page_config, not on nodes. Inset inside a band is a role in Theme and fonts (box_decoration.padding_pt), not margin or padding on a node.

See also (catalog)

NeedFile
Magazine image + copyex_media_row.json
Page-height shell (slide, poster, filled page)ex_poster_shell.json, ex_filled_page.json
Background under contentex_overlay.json
Flowing multi-column articleex_columns.json
All constructsCatalog

Allowed keys

Allowed keys and nodes.schema.jsonlayout.

Common mistakes

  • Replacing content/root.json with a grid or horizontal-stack fragment (root is not document flow)
  • justify_content: "space-between" (not in the schema)
  • CSS-style fields on nodes (margin, padding, x, y)
  • { "fr": 1 } rows without a finite outer height
  • Integer track sizes instead of { "pt": N }
  • Empty spacer containers — use parent gap or theme padding_pt on a dedicated role

Catalog sources

ex_stack.json
{
  "id": "ex.stack",
  "role": "section",
  "content": {
    "type": "container",
    "value": {
      "children": [
        {
          "id": "ex.stack.v1",
          "role": "body",
          "content": { "type": "text", "value": "Vertical stack item one." }
        },
        {
          "id": "ex.stack.v2",
          "role": "body",
          "content": { "type": "text", "value": "Vertical stack item two." }
        },
        {
          "id": "ex.stack.hrow",
          "role": "section",
          "content": {
            "type": "container",
            "value": {
              "children": [
                {
                  "id": "ex.stack.h1",
                  "role": "body",
                  "content": { "type": "text", "value": "Left" }
                },
                {
                  "id": "ex.stack.h2",
                  "role": "body",
                  "content": { "type": "text", "value": "Right" }
                }
              ]
            }
          },
          "layout": {
            "type": "stack",
            "direction": "horizontal",
            "gap": 12000,
            "align_items": "center"
          }
        }
      ]
    }
  },
  "layout": {
    "type": "stack",
    "direction": "vertical",
    "gap": 8000,
    "justify_content": "start"
  }
}
ex_grid.json
{
  "id": "ex.grid",
  "role": "section",
  "content": {
    "type": "container",
    "value": {
      "children": [
        {
          "id": "ex.grid.cell0",
          "role": "body",
          "content": { "type": "text", "value": "Top cell (fr row)." }
        },
        {
          "id": "ex.grid.cell1",
          "role": "body",
          "content": { "type": "text", "value": "Bottom cell (fixed pt row)." }
        }
      ]
    }
  },
  "layout": {
    "type": "grid",
    "columns": [{ "fr": 1 }, { "fr": 1 }],
    "rows": [{ "fr": 1 }, { "pt": 48000 }],
    "gap": 8000,
    "row_gap": 12000,
    "column_gap": 16000,
    "height": 120000,
    "cell_align": { "x": "stretch", "y": "start" }
  }
}