Skip to content

Layout & containers

Layout blocks don’t draw data; they decide where everything sits. There are two layout systems, and reaching for the right one is most of the job:

  • @block/grid is the page’s skeleton — a fixed, pixel-height grid. Use it for the top-level arrangement of a dashboard, where charts and panels hold a stable position and don’t reflow as data loads.
  • @block/stack (and its @block/row / @block/column shorthands) is flow layout — it sizes to its content and flows. Use it inside a grid cell, to line up a handful of controls, or to stack a label above a value.

The single-child containers (@block/box, @block/panel) and the flex containers (@block/stack / row / column) all share the same sizing-and-spacing vocabulary from @block/boxwidth, height, and padding — so once you can size a box you can size any of them. (Grid is the exception: it’s a fixed-height system with its own cols / row_height.) Children go in an items array; the single-child containers take their child inline or under block. For the full prop list, see the block reference.

@block/box

The layout primitive: one child, with sizing (width, height) and inner padding. Reach for it when you just need to pad something, cap a width, or make a child fill its parent. Everything else here builds on these same props.

{ "@block/box": { "padding": "md", "@block/text": "padded content" } }

@block/stack, @block/row, @block/column

Flow layout in one direction. @block/stack takes a direction; @block/row and @block/column are the same thing with the direction baked in (and they accept a bare array of children). Beyond the box props they add gap between children, align_items ('start' | 'center' | 'end') for cross-axis alignment, and justify_content ('start' | 'center' | 'end' | 'space-between' | 'space-around' | 'space-evenly') for main-axis distribution — e.g. justify_content: "end" on a @block/row pushes a control to the far edge.

This is what you use for the small stuff: a strip of controls, a stat with a label above it, the contents of one grid cell.

{
"@block/stack": {
"direction": "row",
"gap": "md",
"align_items": "center",
"items": [{ "@block/text": "left" }, { "@block/text": "right" }]
}
}
{ "@block/row": [{ "@block/text": "left" }, { "@block/text": "right" }] }

@block/grid

The dashboard skeleton — a fixed-height grid on a column track (12 columns by default). Items declare their cols / rows span and optional x / y position; every row is a fixed pixel height (row_height), so the layout doesn’t jump as panels load. Reach for grid at the top of a data-heavy page and drop a chart, table, or panel into each cell.

{
"@block/grid": {
"cols": 12,
"items": [
{ "cols": 12, "rows": 1, "@block/query_bar": {} },
{
"cols": 6,
"rows": 4,
"@block/plot": {
"line_y": { "from": "requests", "y": "p95", "title": "Latency" }
}
},
{
"cols": 6,
"rows": 4,
"@block/plot": {
"line_y": { "from": "requests", "y": "errors", "title": "Errors" }
}
}
]
}
}

@block/panel

A titled card — a heading, an optional description, and a border that separates its body from its neighbours.

Every visualization block is already a panel: it renders inside a card by default and takes the panel props (title, description, chrome, …) directly, so you rarely wrap a chart or table in @block/panel yourself (see Panels). Reach for @block/panel directly when the body is not a single visualization — wrapping a @block/stack, some text, or several blocks in one card:

{
"@block/panel": {
"title": "Settings",
"description": "Manage settings",
"@block/text": "Body content"
}
}

Like a visualization block, @block/panel accepts an optional top-level id — a stable, view-unique name the runtime can address and inspect it by.

@block/tabs and @block/accordion

Progressive disclosure for when there’s more than fits on one screen. Use tabs to show one view at a time behind a tab bar (overview / details / logs); use an accordion to stack collapsible sections the reader can open independently. Both take a map of id to { title, <child> }.

{
"@block/tabs": {
"tabs": {
"overview": { "title": "Overview", "@block/text": "Overview content" },
"details": { "title": "Details", "@block/text": "Details content" }
}
}
}

@block/drawer

Pushes detail off to the side without leaving the page — a slide-out panel opened from a @block/button or a @block/table row (see their click). Its open/closed state is a bound boolean; the authored form pairs that state with the panel’s content.

{
"@block/drawer": [{ "@expr/state": false }, { "title": "Details", "@block/text": "Drawer body" }]
}

@block/fragment

Returns several blocks where one is expected, without adding a wrapper element to the layout. Handy when a template or a branch needs to emit a list of siblings into its parent.

{ "@block/fragment": [{ "@block/text": "First" }, { "@block/text": "Second" }] }

An id alongside a single child names that subtree with a declared node id without adding anything to the layout. Declared ids must be unique wherever the nodes can mount together, so a shared template used more than once must not declare a literal id itself — the instantiation site names it instead:

{
"@block/fragment": {
"id": "requests_recent",
"@block/use": "@frames/shared.templates.json#/block/requests_table"
}
}