Patterns / Canvas

Canvas

The canvas is the surface an assembled layout is read and edited on. It holds blocks of content, each on its own surface with the actions that belong to it, so a reader can take in a composed page and work on any part of it in place.

On this page

Like ConfirmationModal and ErrorView, this one the package ships. Canvas is a module: an arrangement you import rather than one described for you to rebuild, and it arrives in two parts — the board, and the block that sits on it.

What it fixes is the geometry. A block takes a whole row or half of one, there is no third fraction and no column count, and the room around and between them comes from named steps rather than a number you pass. What it leaves alone is the content: a block holds whatever you render inside it, and the board draws them in the order you wrote them.

The board

The board and its blocks are the same two surfaces a Card is built from. The board is the standalone card — --surface-depth-0 over --border-default at --radius-lg — and each block is a Tile on it: --surface-depth-1, --radius-md, and no visible border, because the board has already drawn the outer one. Hover a block and its border comes up to --border-default. That, rather than a shadow, is what says the block is live — a tile does not lift off the surface it is sitting on, and the action colour stays reserved for things you can click.

Pipeline by stage

A full row, because a stage breakdown is wide before it is tall.

Win rate

41%, up four points on the quarter.

Cycle time

18 days from first touch to signature.

Delete all three and the board says so instead of collapsing to nothing. The toolbar only exists because these blocks were given something to put in it: onDelete draws the trash button and actions takes anything else — here a Dropdown on the first block, at the same 36px as the button beside it.

Rows, and half a row

Four blocks: one full, then three halves. The first two halves pair into a row and the third takes half of the next one and leaves the rest empty. It does not stretch — the same markup would otherwise render two different widths depending on what happened to come after it.

Full

The whole row, whatever else is on the board.

Half

Paired with the one beside it.

Half

The other half of that row.

Half, alone

Half of the next row. The rest of it stays empty.

None of these has a toolbar, because none was given an onDelete or any actions. That is the block for content nobody acts on — readOnly is for a board someone is only reading, which is a different thing.

Padding and gap

The default, then padding="comfortable" with gap="regular", then padding="none". Watch the halves as the gap changes: a half block is 50% less half the gap, so widening the gap narrows both of them rather than pushing the second onto its own row.

Regular, compact

20px around, 8px between.

Regular, compact

The default board.

Comfortable, regular

24px around, 16px between.

Comfortable, regular

Room for blocks that each carry weight.

None

Flush to the board edge.

None

For a canvas already inside a padded panel.

Read-only

The same three blocks, each still passed an onDelete, on a board with readOnly. Every toolbar is gone rather than disabled, so there is no chrome implying an action that would not work, and the blocks stop answering the cursor. Set it on a single block to lock one of them inside a board that is otherwise editable.

Pipeline by stage

A full row, because a stage breakdown is wide before it is tall.

Win rate

41%, up four points on the quarter.

Cycle time

18 days from first touch to signature.

Filling a column

height="fill" inside a 320px flex column. The board takes what is left of the column and the blocks scroll inside it, so the board's own edges stay where they are — which is what a builder or a preview pane wants. Give it a parent that is not a flex column and it grows with its content instead, which is what auto already does.

Sessions

Enough blocks that the board has to scroll.

Signups

Enough blocks that the board has to scroll.

Activation

Enough blocks that the board has to scroll.

Retention

Enough blocks that the board has to scroll.

Expansion

Enough blocks that the board has to scroll.

Churn

Enough blocks that the board has to scroll.

There is no maxHeight. The board already scrolls, so a height belongs in your own rule on [data-slot="canvas"] — beside whatever else decides that layout, rather than passed control by control.

Nothing in it

An empty board says what is missing and draws no frame around it. A dashed border means a drop target and nothing here accepts one, so putting one there would be a promise the component cannot keep.

Add a section to start building this page.

When to use

Reach for the canvas when a screen is an assembly of blocks rather than one piece of content — a builder, a dashboard, a report someone edits in place. What it fixes is the geometry: a block takes a whole row or half of one, and the room around and between blocks comes from named steps rather than a number you pass, so every board in a product spaces itself the same way. What it leaves alone is the content, which is whatever you render inside a block. The habit to break is putting anything else straight on the board: a heading dropped there becomes a flex item sized to its own text rather than a row across the width, and it has none of the block’s surface under it. A heading for the board goes above the canvas; a heading for a block goes inside the block.

When not to use

A single surface carrying one reading and one action is a Card, and a Card is what a block holds rather than what replaces the board. The other way to miss is to reach for a module where the arrangement is already settled by the page: a two-column form, a filter row above a table. The tell is whether the blocks come and go — a set of surfaces known when you write the markup is a layout of your own, and the board would only be a heavier way of spelling it.

padding

How much room the board leaves around its blocks. Regular is 20px and is the answer unless something specific argues otherwise.

paddingWhen to use
noneBlocks flush to the board edge. For a canvas that is itself inside a padded panel, where a second inset would double the first.
regular20px. The default, and the right answer most of the time.
comfortable24px. For a board with few blocks and room to spare, where the extra air reads as deliberate rather than empty.

gap

The space between blocks, in both directions. It also sets what half of a row is: a half block is 50% less half the gap, so the two axes cannot be set independently and a wider gap gives narrower halves rather than an overflowing row.

gapWhen to use
compact8px. The default. Blocks read as parts of one board, which is what you want when there are more than a few.
regular16px. For a handful of blocks that are each substantial — a chart apiece — where the tighter gap makes them look crowded.

height

Whether the board sizes to its blocks or to the space it is given. Fill is what a working surface wants — the blocks scroll inside a board that stays put — and it needs a parent that is a flex column, because it claims the column’s remaining height rather than measuring anything.

heightWhen to use
autoThe board grows with its blocks and the page scrolls. The default, and correct wherever the canvas is one section of a longer page.
fillThe board takes the rest of its flex column and scrolls internally. For a canvas that is the screen: a builder, an editor, a preview pane.

size

A block claims a whole row or half of one. There is no third fraction and no column count — the row is the unit, so blocks wrap without any of them knowing where they sit.

sizeWhen to use
fullThe whole row. The default, and what anything wide belongs in: a table, a chart with a legend, a paragraph of prose.
halfHalf the row, so two sit side by side. One on its own leaves the other half of the row empty rather than stretching, which is the honest rendering — pair them, or make it full.