Layout primitives
SLASHED ships breakpoint-free, container-query-driven layout primitives in the
slashed.layout layer (core/layout.css). Each is a single class with
per-instance tokens you can override inline (style="--sf-stack-gap: …").
Tokens are declared in core/tokens.layout.css.
All primitives are exercised live in the demo.
The primitives
| Class | What it does | Key tokens |
|---|---|---|
.sf-section |
vertical page rhythm; sizes --xs/--s/--m/--l/--xl/--2xl |
--sf-section-pad* |
.sf-section-group |
collapses the gap between adjacent sections | — |
.sf-container |
centered max-width wrapper; declares the named sf-layout container; --narrow |
--sf-container-*, --sf-gutter |
.sf-box |
isolated unit with padding and optional border outline | --sf-box-padding, --sf-box-border-width, --sf-box-border-color |
.sf-center |
intrinsic centering with max-width and gutters; --intrinsic |
--sf-center-max, --sf-center-gutter |
.sf-place-center |
box-neutral centring: centres content on both axes (grid + place-items:center) in a plain element |
— |
.sf-stack |
vertical flow with consistent gap (the “owl”) | --sf-stack-gap |
.sf-cluster |
wrapping inline group; --no-wrap |
--sf-cluster-gap/-align/-justify |
.sf-sidebar |
content + fixed-ish side panel that wraps when narrow | --sf-sidebar-* |
.sf-switcher |
N columns above a threshold, stacked below; --no-wrap, --vertical |
--sf-switcher-threshold/-gap |
.sf-grid |
auto-fill responsive grid; --fit, --xs … --2xl, --dense |
--sf-grid-min, --sf-grid-gap |
.sf-grid-cols-1 / -2 / -3 / -4 / -6 |
fixed-column grids, container-responsive (no -5) |
--sf-grid-gap |
.sf-grid-cols-1-2 / -2-1 / -1-3 / -3-1 |
ratio two-column grids | --sf-grid-gap |
.sf-bento |
dense free-form grid; container modifiers --2/--3/--6, --row-compact/--row-tall; child span classes .sf-bento-wide/-full/-tall/-featured |
--sf-bento-* |
.sf-alternate |
zigzag two-column layout, reverses every other row; CQ-responsive | --sf-content-gap, --sf-gap |
.sf-pancake |
sticky-footer grid: header / main(1fr) / footer | — |
.sf-content-grid |
breakout layout; children .sf-breakout, .sf-full-bleed; establishes an inline-size CQ scope (like .sf-container) so .sf-grid-cols-*/.sf-bento still respond when it replaces a container |
--sf-content-width, --sf-breakout-width |
.sf-grid-flex |
flex-based grid alternative for uneven item counts; last-row leftovers stretch to fill (default) or stay fixed and centered (--center); --xs … --2xl |
--sf-grid-min, --sf-grid-gap |
.sf-cover |
full-height region with a centered .sf-cover__center; --min/--max/--padding-* |
--sf-cover-* |
.sf-frame |
aspect-ratio media box | --sf-frame-ratio |
.sf-bg-layer |
cover media layer behind the parent’s content (parent auto-positions + isolates); composes under .sf-scrim |
--sf-bg-layer-inset/-fit/-position/-radius/-z |
.sf-reel |
horizontal scroll strip | --sf-reel-*, --sf-mask-scrim-* |
.sf-imposter |
absolutely-centered overlay; --fixed, --contain |
--sf-imposter-margin |
.sf-subgrid / .sf-subgrid-rows |
inherit parent grid tracks | — |
.sf-divider |
token-driven separator; --vertical, --soft, --strong, --dashed, --dotted, --gradient |
--sf-divider-* |
.sf-gap |
injects gap into any existing flex/grid container without imposing display; --xs … --2xl |
--sf-gap |
.sf-equal |
flowing multi-column layout (CSS columns, not grid) — content distributes like a newspaper column instead of sitting in fixed cells; --2/--3/--4/--6 set a target column-count |
--sf-equal-min-col, --sf-equal-gap, --sf-equal-rule-* |
.sf-section--collapse |
adjacent sections share padding 50/50 across the boundary | --sf-section-pad |
.sf-icon |
em-based inline icon sizing; --xs … --2xl |
--sf-icon-* |
.sf-icon--boxed |
padded, bordered icon frame (content-box sizing) | --sf-icon-box-pad, --sf-icon-box-radius, --sf-icon-box-bg, --sf-icon-box-border |
.sf-bento has two distinct, non-interchangeable modifier families — a --
(double-dash) class always modifies the container (.sf-bento itself), a
- (single-dash) class targets a child placed inside it:
| Class | Applies to | Effect |
|---|---|---|
.sf-bento--2 / --3 / --6 |
container | sets the column count |
.sf-bento--row-compact / --row-tall |
container | sets the row-height tier for every auto row in the grid |
.sf-bento-wide |
a child | that item spans 2 columns |
.sf-bento-full |
a child | that item spans the full row |
.sf-bento-tall |
a child | that one item spans 2 rows |
.sf-bento-featured |
a child | that item spans 2 columns and 2 rows |
.sf-bento--row-tall and .sf-bento-tall are not aliases: the first
stretches every row in the grid, the second stretches one grid item. Put the
container modifier on .sf-bento, the child modifiers on the items inside
it — never on the same element.
Size-modifier scale – xs..2xl everywhere
Every size-aware primitive supports the canonical range
--xs --s --m --l --xl --2xl.
| primitive | what the suffix scales | --xs |
--s |
--m (default) |
--l |
--xl |
--2xl |
|---|---|---|---|---|---|---|---|
.sf-stack |
block-axis gap | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ |
.sf-cluster |
inline-axis gap | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ |
.sf-gap |
layout-agnostic gap | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ |
.sf-grid |
min column width | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ |
.sf-grid-flex |
min item width | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ |
.sf-section |
block padding | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ |
.sf-icon |
font-size | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ |
Three suffixes scale different physical dimensions. .sf-grid--xs means “narrow
column tier” (denser layouts), not “small gap” — gap on .sf-grid is controlled
separately by --sf-grid-gap, independent of the column-min modifier. To go beyond
the built-in scale, override the scoped token directly:
style="--sf-stack-gap: var(--sf-space-3xl)". The underlying space tokens
(--sf-space-2xs/-3xl/-4xl) are part of the public token API.
Why no breakpoints
Primitives respond to their own container width via @container, not the
viewport. Drop a .sf-grid-cols-3 inside any width context and it adapts — no media
queries, no breakpoint tokens. See the
container query contract for the
two named containers (sf-layout, sf-alternate) and when to use them.
Composing
Primitives nest freely:
<section class="sf-section">
<div class="sf-container">
<div class="sf-stack">
<h1>Title</h1>
<div class="sf-grid sf-grid--m">
<article class="sf-stack sf-stack--s">…</article>
<article class="sf-stack sf-stack--s">…</article>
</div>
</div>
</div>
</section>
Override a single instance without new CSS:
<div class="sf-cluster" style="--sf-cluster-gap: var(--sf-space-l)">…</div>
Responsive gaps
Gaps are already fluid: --sf-grid-gap defaults to --sf-gap, which interpolates
between its mobile and desktop ends via the shared space scale. Retune the whole
rhythm at once with the space-scale endpoints (--sf-space-base-min /
--sf-space-base-max) rather than per-token knobs.
When one primitive needs a different gap on small vs large screens, override
its scoped token inside a @container query — the same container-driven model the
primitives themselves use, so the gap reacts to the same width that collapses the
columns. Endpoints stay live tokens, so they still follow any scale retuning:
.product-grid { --sf-grid-gap: var(--sf-space-l); }
@container (min-width: 48rem) {
.product-grid { --sf-grid-gap: var(--sf-space-xl); }
}
<div class="sf-container">
<div class="sf-grid product-grid">…</div>
</div>
The override needs a container ancestor (.sf-container or .sf-cq) — the same
requirement as .sf-grid-cols-*. The gap steps at the breakpoint rather than
interpolating across it; for a gap that single step is imperceptible in normal use.
The same pattern works for any scoped gap token (--sf-gap, --sf-content-gap,
--sf-gutter, --sf-cluster-gap, …).
Container-query scope
The container-responsive primitives (.sf-grid-cols-*, .sf-bento, and any
@container-scoped token override like the one above) resolve against the
nearest ancestor that establishes an inline-size query container. Two wrappers
establish one for you: .sf-container and .sf-content-grid — so either can
host those primitives directly.
.sf-center deliberately does not. It’s a minimal centring primitive with
no side effects (it doesn’t become a containing block for position: fixed
descendants or a new stacking context), so it stays composable. When you need a
container-responsive child inside a centred wrapper, add the query scope
explicitly by composing .sf-cq:
<div class="sf-center sf-cq">
<div class="sf-grid-cols-3">…</div>
</div>
Centring: pick the primitive, not a .center utility
Coming from a utility framework, .sf-center is a false friend. There, a
.center class usually centres the content inside it (flex place-items,
margin: auto, text-align). In SLASHED that job belongs to
.sf-place-center; .sf-center is the Every-Layout Center primitive: a
max-width, guttered wrapper that centres itself in its parent — it does not
centre its own children (unless you add --intrinsic). The two are a pair:
.sf-center centres itself, .sf-place-center centres its content.
.sf-place-center is the box-neutral centring primitive — display: grid;
place-items: center — the named form of the community
display:grid; place-items:center idiom. Reach for it when you have a plain
element and just want its content centred on both axes, without adopting a
row/column/wrap primitive:
<div class="sf-place-center" style="min-block-size: 12rem">
<p>centred on both axes</p>
</div>
Otherwise centring is expressed as intent on the right primitive:
| To centre… | Use | Mechanism |
|---|---|---|
| content in a plain box, both axes | .sf-place-center |
grid + place-items |
| a block wrapper horizontally | .sf-center (prose measure) or .sf-container (app wrapper, adds CQ) |
margin-inline: auto |
| children in a row | .sf-cluster--center |
justify-content |
| children in a column | .sf-stack--center |
align-items |
| leftover grid items | .sf-grid-flex--center |
justify-content |
| an element on both axes (overlay) | .sf-imposter |
absolute + translate |
| content in a full-height region | .sf-cover__center |
margin-block: auto |
| a wrapper’s children by their intrinsic width | .sf-center--intrinsic |
flex column + align-items |
Two things to remember about .sf-center specifically:
- It centres itself, not its content. For content-centring reach for the modifiers above.
- It needs a block-level element in normal flow.
margin-inline: autodoesn’t centre an inline element, and inside a flex or grid container the parent’s layout takes over the item’s sizing and alignment, so the primitive’s ownwidth/marginmodel no longer drives the result..sf-centeriscontent-box, so--sf-center-maxbounds the content and the gutter sits outside it — pick it over.sf-containerwhen you want a typographic measure; pick.sf-containerwhen you want an app-level wrapper that also establishes a container-query scope.