Layout
Farn's layout philosophy is content-first: let the content determine the container size, not the other way around. Width tokens give you three semantic slots. Structural HTML gives you meaningful landmarks. Grid and flex each have a clear job.
Content-first widths
Three width tokens cover the full range of layout needs. Apply them to the element that owns the constraint — usually a wrapper <div> with margin-inline: auto.
70ch is relative to the element's own font size, so prose columns naturally widen slightly for display headings and narrow for captions — no extra math required. For the full spacing reference including padding scale and z-index, seeStyles › Spacing.
Width token reference
| Token | Value | Use |
|---|---|---|
--width-content | 1080px | Outer page container — navigation, hero, section wrappers |
--width-prose | 70ch | Text column — articles, docs, any running prose |
--width-narrow | 640px | Tight containers — forms, dialogs, focused card layouts |
Page structure
Use semantic landmarks. The wireframe below shows a typical page skeleton and the natural width token pairing for each landmark.
Page structure code
<body>
<header>
<!-- max-width: var(--width-content) -->
<nav>...</nav>
</header>
<main>
<!-- outer bound: var(--width-content) -->
<article>
<!-- prose column: var(--width-prose) -->
<h1>...</h1>
<p>...</p>
</article>
<aside>
<!-- sidebar or supplemental: var(--width-narrow) -->
</aside>
</main>
<footer>
<!-- max-width: var(--width-content) -->
</footer>
</body>Grid vs flex
Flex for one-dimensional alignment, grid for two-dimensional layout. The demo below usesrepeat(auto-fill, minmax(180px, 1fr)) — columns appear as space allows, collapsing to a single column on narrow viewports without a media query.
Grid vs flex pattern reference
| Pattern | Use | Example |
|---|---|---|
| Flex row | Aligning items along a single axis | Nav links, button group, icon + label |
| Flex column | Stacking with controlled spacing | Form fields, card body |
| Grid columns | Placing items in explicit rows and columns | Card grid, two-column doc layout |
| Grid auto-fill | Responsive columns without breakpoints | repeat(auto-fill, minmax(240px, 1fr)) |
Always use gap: var(--space-*) rather than margins between grid or flex children. This keeps spacing in the token system and avoids margin-collapse edge cases.
Layout composition primitives
farn-layout.css is an opt-in layer of ten composition classes built on top of the Farn token set. Load it after farn.css or farn-tokens.css. The classes use Every Layout naming — they describe intent, not CSS properties.
@import "farn-theme"; /* tokens + reset */
@import "farn-theme/layout"; /* opt-in: container, stack, cluster… */Containers
Three width-constrained, centered wrappers. Each sets container-type: inline-sizeso @container queries on children fire without an extra wrapper. Padding reduces from --space-lg to --space-md at mobile widths.
Container reference
| Class | Max-width token | Typical use |
|---|---|---|
.container | --width-content (1080px) | Page sections, nav wrapper, hero, footer |
.container-narrow | --width-narrow (640px) | Forms, dialogs, focused card layouts |
.container-prose | --width-prose (70ch) | Articles, documentation, running prose |
All three set box-sizing: border-box, margin-inline: auto, and padding-inline: var(--space-lg) (reduced to var(--space-md) at <640px). The box-sizing guard ensures padding is absorbed into the max-width even when loading farn-tokens.css without farn.css's base reset.
Name collision: .container is a common class name. If your existing CSS already defines .container, loading farn-layout.css may conflict — audit your styles or use farn-tokens.css to adopt only the tokens without the classes.
<section>
<div class="container">
<!-- content constrained to 1080px -->
</div>
</section>Stack, Cluster, Center
The three fundamental composition primitives. Stack flows children vertically. Cluster wraps them horizontally. Centerplaces a single item at the middle of both axes. All accept inline custom-property overrides for gap.
.stack.cluster.centerStack / Cluster / Center reference
| Class | CSS | Override |
|---|---|---|
.stack | flex-direction: column; gap: var(--space-md) | --stack-gap |
.cluster | flex-wrap: wrap; align-items: center; gap: var(--space-md) | --cluster-gap |
.center | display: flex; align-items: center; justify-content: center | — |
<!-- Tighter gap on this stack -->
<div class="stack" style="--stack-gap: var(--space-sm)">
<div class="card">…</div>
<div class="card">…</div>
</div>
<!-- Badge row -->
<div class="cluster">
<span class="badge badge-general">Tag</span>
<span class="badge badge-published">Live</span>
</div>Auto grid
Columns auto-fit to available space using auto-fit minmax() — no breakpoints needed. Override the minimum column width with --grid-min (default: 280px). This is the general-purpose version; .card-grid in farn-components.cssuses the same pattern with a fixed 280px minimum.
col 1col 2col 3col 4col 5Auto grid reference
| Override | Default | Effect |
|---|---|---|
--grid-min | 280px | Minimum column width before wrapping |
--grid-gap | var(--space-md) | Gap between columns and rows |
<!-- Tighter columns, smaller gap -->
<div class="auto-grid" style="--grid-min: 200px; --grid-gap: var(--space-sm)">
<div class="card">…</div>
<div class="card">…</div>
<div class="card">…</div>
</div>vs .card-grid: .auto-grid is in farn-layout.cssand accepts --grid-min. .card-grid is in farn-components.csswith a fixed 280px minimum — use it when you always want the card column width, .auto-gridwhen you need a different minimum or want to override per-instance.
Sidebar, Grid-2, Grid-3
Explicit column grids. Sidebar pairs a fixed-width column with a flexible main area. Grid-2 and Grid-3 create equal columns. All three collapse responsively when inside a container context — wrap them in.container (or any element with container-type: inline-size) to activate @container collapsing.
.sidebar — collapses to 1 col at <600px container width.grid-2 — collapses to 1 col at <500px.grid-3 — 3→2 cols at <900px, 1 col at <500pxSidebar / Grid-2 / Grid-3 reference
| Class | Columns | Collapses (in container context) | Overrides |
|---|---|---|---|
.sidebar | var(--sidebar-w, 300px) 1fr | 1 col at <600px | --sidebar-w, --grid-gap |
.grid-2 | repeat(2, 1fr) | 1 col at <500px | --grid-gap |
.grid-3 | repeat(3, 1fr) | 2 cols at <900px, 1 col at <500px | --grid-gap |
Container context required for collapsing. Grids collapse via@container queries, not @media queries. Wrap in.container or set container-type: inline-size on a parent.
<!-- Grids collapse when the .container is narrow -->
<section>
<div class="container">
<div class="grid-3">
<div class="card">…</div>
<div class="card">…</div>
<div class="card">…</div>
</div>
</div>
</section>
<!-- Sidebar with narrow sidebar column -->
<div class="container">
<div class="sidebar" style="--sidebar-w: 240px">
<nav>…</nav>
<main>…</main>
</div>
</div>Radius as hierarchy
Border radius is not purely decorative — it signals depth and interactivity. Use the scale consistently so elements at the same depth read as related.
--radius-sm4px · badges, code--radius-md6px · buttons, inputs--radius-lg8px · cards, panels--radius-xl12px · hero cards--radius-full9999px · pillsNest radii correctly: an inner element should have a smaller radius than its container, roughly container-radius - padding. Matching radii at every level makes nested surfaces look misaligned. See Styles › Spacing for the full radius token reference.
Stacking order
Eight z-index tokens define a predictable stacking ladder. Never hardcode z-index values — use the tokens so layers stay relative to each other as the system grows.
--z-base · 0Normal flow--z-raised · 1Floated cards--z-content · 10Sticky sub-nav--z-dropdown · 100Dropdowns, popovers--z-sticky · 200Sticky header--z-modal · 400Modal dialog--z-toast · 500Toasts, notificationsThe stacking order matches the visual hierarchy: page content sits at the bottom, transient UI (toasts, modals) at the top. Never set a dropdown above a modal — if a component needs to break this order, the design decision is worth questioning first.