Skip to content

Layout

Layout

App surfaces in Bolt are composed from a small set of layout primitives from @norbital-ai/ui/layout . The primitives own geometry; the shell owns the app region and document scroll; and every surface follows the same body contract. Compile-time checks enforce this — a surface that breaks the rules fails the build, not just the review.

The primitives

Pick the primitive by the layout intent, not by what looks closest:

IntentPrimitiveNotes
Vertical rhythmStackColumn of children with gap , optional align / justify
One rowInlineSingle horizontal line of children
Wrapping groupClusterRow that wraps; tags, chips, button groups
Two adaptive regionsSplitNamed ratios ( rail , sidebar , third , half , wide ) and shared collapse tokens
Intrinsic gridGridAuto-fit cells from a minimum token ( compact / card / panel
Exact spansColumns + ColumnFixed counts (2/3/4/6) with explicit span
Top, main, bottomCoverCenters the body between optional top and bottom
Readable measureCenterConstrain width to a measure ( narrow / reading / wide / full
Media cropFrameFixed aspect ratio ( square / portrait / landscape / widescreen
Local scrollingBound + ScrollAlways a pair — never a bare overflow

All primitives accept gap and pad from the token scale ( none , xs , sm , md , lg , xl ), an as element (a whitelist of flow containers such as section , article , main , form ), and their named props. Parents choose the layout algorithm; children do not request growth.

The app body contract

Every app is one Cover with a top heading slot and exactly one body region:

┌───────────────────────────────────────────────┐
│ Cover  top={pageHeading}                      │
│   ┌─────────────────────────────────────────┐ │
│   │ PageHeader title="Tasks" …              │ │
│   └─────────────────────────────────────────┘ │
│                                               │
│   body region (one of three, below)           │
│   ┌─────────────────────────────────────────┐ │
│   │ Scroll name="tasks" inset               │ │
│   │   ┌───────────────────────────────────┐ │ │
│   │   │ CollectionTable collection="tasks"│ │ │
│   │   └───────────────────────────────────┘ │ │
│   └─────────────────────────────────────────┘ │
└───────────────────────────────────────────────┘

There are exactly three legal body regions, and each owns both scroll and the app inset:

BodyOwns scroll + inset
<Tabs … />, automatically
<Scroll name="…" inset> for flowing contentthe
<Bound size="full" inset> for one self-scrolling componentthe
<script lang="ts">
  import { CollectionTable } from '@norbital-ai/ui/collection-table';
  import { Bound, Cover, Scroll } from '@norbital-ai/ui/layout';
  import { PageHeader } from '@norbital-ai/ui/page-header';
</script>

<svelte:head>
  <title>Tasks</title>
  <meta name="bolt:icon" content="lucide:list-todo" />
</svelte:head>

{#snippet pageHeading()}
  <PageHeader title="Tasks" description="Everything the team is working on." />
{/snippet}

<Cover as="main" top={pageHeading}>
  <Scroll name="tasks-scroll" inset>
    <CollectionTable collection="tasks">
      {#snippet columns({ Column })}
        <Column name="title" />
        <Column name="status" />
      {/snippet}
    </CollectionTable>
  </Scroll>
</Cover>

Scrolling: one owner per axis

The shell owns the document scroll; every local scroll region is an explicit Bound + Scroll pair. Each ancestor chain has one scroll owner per axis and one inset owner ; sibling panes may own their own. The rules that fall out of this are enforced at compile time:

  • No generic overflow wrappers, overflow-hidden clipping, or raw flex/min-size scroll chains.
  • No raw structural flex/grid containers, and no margins between siblings — spacing is the primitives’ gap .
  • No literal px-4 sm:px-6 -style classes — the app inset is the primitives’ inset .
  • No style attribute on a layout primitive — use its named props.
  • Clipping is valid only for text truncation, Frame media, or an audited popup/sheet boundary.

Responsiveness without breakpoint recipes

  • Split takes collapse ( stack , switch , none ) and a shared collapseAt token instead of arbitrary widths.
  • Grid is intrinsic: it auto-fits cells to the available space from a minimum token. Use Columns only when an exact count or span matters.
  • Center keeps reading width without hand-rolled media queries. Prefer intrinsic layout and shared container-query tokens over viewport breakpoint recipes.
Enforced, not suggested
The compiler validates the layout contract on every surface: unknown layout props, raw clipping, scroll chains, and inline styles on primitives are diagnostics, and authored source outside src/apps/ is checked the same way. See UI components for how forms fit into a surface.