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/gridis 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/columnshorthands) 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/box — width, 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" }}