Skip to content

Layout

Layout components arrange their children. They are containers — pass children to ->schema([...]), and nest them freely. This page covers the three you reach for most; the disclosures (Section & Collapsible) and the corner-pinned Floating panel have their own pages.

Containers do — never the content. A form, a heading, a paragraph and a table all fill whatever contains them, so the same definition looks right in a card, a tab, a grid column and a modal without knowing which one it landed in.

That leaves two places to set a measure:

  • The page, via #[AsPage(width: PageWidth::…)]. Caps the whole page and centres it in the layout slot, so a heading and the form beneath it stay aligned.
  • A container in the schema, via Stack::make()->width(Width::…), when one zone of a page needs a narrower measure than the rest — a settings form beside a full-width table, say.

Forget both and content stretches to the full content width. That is the intended failure mode: a stretched form is obvious on sight, whereas a component quietly overriding its container’s layout is not.

Stack::make() lays children out in one direction with a consistent gap. It defaults to a vertical column; pass ->direction('row') to lay them out horizontally.

Stack::make()->direction('row')->gap(Gap::Small)->schema([
Button::make('Save'),
Button::make('Cancel')->emphasis(Emphasis::Ghost),
]);

Tune the layout with enum-typed options (see the enums reference):

  • ->gap(Gap::ExtraSmall … ExtraLarge) — the space between children.
  • ->align(Align::…) and ->justify(Justify::…) — cross-axis and main-axis alignment.
  • ->width(Width::…) and ->height(Height::…) — sizing. The capped widths (SmallExtraLarge) centre the stack; add ->float(Side::Start) or ->float(Side::End) to pin it to one edge instead.
  • ->float(Side::…) — float the stack to one side.
  • ->sticky() — pin the stack below the sticky chrome above it (a sticky Topbar, an earlier sticky stack) while the page scrolls. The stack paints the page background, keeps a small gutter once stuck, and publishes its own height as the sticky offset for its siblings, so a sticky vertical tab rail or another sticky stack further down lines up beneath it. A page header with the title and primary actions is the typical candidate.

Grid::make()->columns(…) lays children out in a responsive CSS grid:

  • ->columns(3) — three equal columns from the md breakpoint up, one column below.
  • ->columns(['default' => 1, 'md' => 2, 'xl' => 4]) — an explicit count per breakpoint (default, sm, md, lg, xl, 2xl), mobile-first: each value applies from its breakpoint up until a larger breakpoint overrides it.
  • ->columns('2fr 1fr 1fr 1fr') — a raw grid-template-columns track list for unequal columns, e.g. one wide plus three narrow. Track lists work inside breakpoint maps too.

Any child component or field controls how many columns it covers with ->columnSpan(n) (from md up), a breakpoint map, or ->columnSpanFull() to stretch across the whole row.

Grid::make()->columns(3)->schema([
Badge::make('Wide')->columnSpan(2),
Badge::make('Narrow'),
Badge::make('Full width')->columnSpanFull(),
]);

Every component can opt out of rendering at a breakpoint range: ->hiddenFrom(Breakpoint::Md) hides it at md and up, ->visibleFrom(Breakpoint::Md) shows it only from md up. The toggle is CSS-only (a display: contents wrapper), so it is layout-transparent inside flex and grid parents — use it for chrome that differs between mobile and desktop, like a logo mark that only appears in a mobile topbar.

Card::make('Title', 'Description') wraps children in a bordered panel. Both arguments are optional — omit them for a plain panel. Call ->tooltip('…') to attach an ⓘ info popover next to the title; its content is trusted HTML and may include links.

Card::make('Team settings', 'Manage how your team appears.')
->tooltip('These settings affect everyone on the team.')
->schema([
Stack::make()->gap(Gap::Small)->schema([
Heading::make('Members', 2),
Text::make('Three people have access to this team.'),
Badge::make('3 active'),
]),
Button::make('Invite member'),
]);