Layouts
How layout and composition are handled.
When you build a page in Marigold, it lives inside the AppShell, the fixed frame that every page shares, in a Page that holds the content. Inside that frame, you divide the content into Panels and arrange it with primitives. Rooms, then furniture.
Panel
Panel is a titled surface that renders a visible page section with a title, body, and optional actions. It is the canonical way to divide a page in Marigold.
Most pages are made of one or more Panels. A small view like a settings tab is often a single Panel. A denser view like a dashboard or a detail page is typically several Panels, each one naming a self-contained part of the page. Reaching for Panel is the default. Reaching for something else should be deliberate.
Panel also works outside the AppShell. A login form wrapped in a Panel gives the same titled surface you would use inside an app, so the opinion "use a Panel" holds on pre-auth screens too.
Panel owns the surface, the title, and the body wrapper, not the arrangement inside it. Content layout is the primitives' job, and they work the same way wherever you use them. A common shape is a Stack of fields with an Inline of buttons at the bottom, but any primitive composition is valid.
See the Panel component docs for props, variants, and sizing.
Layout primitives
Layout primitives are invisible positioning components that arrange and space whatever content is placed inside them. Stack flows content vertically, Inline flows horizontally, Columns splits proportionally, and so on.
Without them, web layout is a combination of <div> elements with flex or grid utility classes. That works for a single container, but it scales badly. The moment a section needs a vertical stack containing a row of buttons, wrappers start nesting and each one carries its own class string:
<div className="flex flex-col gap-4">
<h3>Title</h3>
<p>Body text…</p>
<div className="flex justify-end gap-2">
<button>Cancel</button>
<button>Save</button>
</div>
</div>Nothing is broken here, but reading it means parsing every class string to reconstruct the intent. Repeat the pattern across a codebase and layout decisions end up scattered through utility classes, reinvented slightly differently on every page. Marigold replaces this with named layout components:
<Stack space="regular">
<h3>Title</h3>
<p>Body text…</p>
<Inline space="small" alignX="right">
<button>Cancel</button>
<button>Save</button>
</Inline>
</Stack>Each wrapper states what it is rather than how it is configured. You can scan the tree and see the layout flow without reading a single class name. This gives you:
- Named intent. The JSX tells you the layout pattern (
Stack,Inline,Columns) instead of encoding it in class strings the reader has to decode. - One canonical pattern. The same shape looks the same everywhere. Teams don't reinvent a vertical stack in four slightly different ways.
- Composability. A
Buttondrops into any layout without carrying layout opinions of its own.
For these benefits to hold, components must never set their own outer margin. Adding margin to a component causes it to affect, or be affected by, elements outside its boundary, breaking the modularity and composition assumptions that make components reusable in the first place.
Whitespace, positioning, and arrangement are always the responsibility of a parent. A button does not know what sits next to it. A Stack decides the gap between the button and its siblings. This keeps components portable and whitespace predictable across pages.
Spacing between elements is expressed with semantic tokens like regular or group. See the Spacing page for the full vocabulary.
Choosing the right primitive
Most layouts can be built from more than one combination of primitives. Each primitive, though, is built around a specific situation, and reaching for the one that fits most directly keeps pages consistent across a codebase. The table names that situation for each primitive, with a typical example.
| Component | Reach for it when… | Example |
|---|---|---|
| Aside | two elements pair side-by-side where one is fixed-width and the other fills the rest | media-object: thumbnail + details Stack |
| Aspect | a media element must keep a specific aspect ratio regardless of container width | video or iframe embeds, image thumbnails inside Tiles |
| Center | content needs horizontal centering, optionally capped at a max-width | auth screens, empty-state wrappers |
| Columns | a fixed number of regions need explicit proportional widths and should collapse to one column below a breakpoint | label + input rows, sidebar + main page splits |
| Container | content needs a max-width optimized for reading, so long-form text keeps a comfortable line length | article bodies, docs pages, editorial prose |
| Grid | specific children must occupy or span named zones, making the layout structure itself the contract | bento dashboards, detail pages with named regions |
| Inline | multiple elements need to sit side-by-side and wrap to a new line when space runs out | action row of buttons, often with a Split between groups |
| Inset | content inside a sealed surface needs explicit, controllable interior padding | interior padding for Tray, Panel, and other surfaces |
| Scrollable | content may overflow its bounded height or width and needs controlled scrolling | fixed-height table with a sticky header |
| Split | siblings inside an existing Inline or Stack need to be pushed to opposite ends | inside Inline, between a title and its actions |
| Stack | multiple elements need to flow top-to-bottom with a consistent gap | skeleton of most forms and page sections |
| Tiles | a homogeneous set of items of unknown count should fill the row at a minimum child width and reflow automatically | card grids, dashboard metric grids |
Reach for a layout component only with two or more children
A layout component's whole job is to arrange elements in relation to one another. Just like Inline and Stack express the named intent "lay these out horizontally" or "stack these vertically", that intent only exists once there is more than one thing to arrange. A layout component therefore earns its place only when it arranges two or more children. With a single child there is nothing to arrange, so render the element directly instead of wrapping it.
// Don't: a layout component around a single child adds no intent
<Stack>
<Card>…</Card>
</Stack>
// Do: render the element directly
<Card>…</Card>This keeps the JSX tree honest: every layout component in the markup signals a real arrangement decision, not incidental nesting.
Padding and centering wrappers are different
Components whose intent is to wrap a single element — Inset for padding
and Center for centering — legitimately take one child. The two-or-more rule
applies to arrangement components like Stack, Inline, Columns, Grid
and Tiles, where a single child carries no arrangement.
Table is for data, not layout
Table must never be used as a layout grid. It expresses tabular data with
rows and columns of related values, not a way to align arbitrary elements. For
arranging UI, reach for a layout component instead: form fields belong in
Stack, Inline or Columns, never in Table.Cell, since putting inputs
in table cells to line them up breaks the named-intent model and harms
accessibility. See the Table component for its
legitimate use, and the Forms pattern for laying
out fields.