API
Layout
The band grid: a layout caps and centers its children; a child picks its band.
data-ui="layout" (or <ui-layout>) turns an element into the band grid for its own children. Each child sits in the default band, xl, unless placed elsewhere. A layout never styles its parent.
--band: layout-md.<ui-layout style="--gap: 4">
<div data-ui="card" style="--p: 4">Default band (xl)</div>
<div
data-ui="card"
style="
--band: layout-md;
--p: 4;
"
>
--band: layout-md
</div>
<div
data-ui="card"
style="
--band: layout-bleed;
--p: 4;
--rounded: 0;
"
>
--band: layout-bleed
</div>
<div
data-ui="card"
style="
--band: layout-sm;
--band--lg: layout-xl;
--p: 4;
"
>
--band: layout-sm, --band--lg: layout-xl
</div>
<ui-layout data-layout-size="md" style="--band: layout-bleed">
<div data-ui="card" style="--p: 4">nested layout, data-layout-size="md"</div>
</ui-layout>
</ui-layout>/**
* layout.css — Layout (ui-layout | [data-ui~="layout"]): the band grid
*
* @layer variables, components
* @requires _layers.css, _variables.css (--gutters), _breakpoints.css (--layout-*),
* _properties.css (--_band-resolved)
* @uses named grid lines + implicit named areas: `--band: layout-md` on a
* child resolves through `layout-md-start / layout-md-end`
* @uses grid-template-columns: subgrid — a layout that is a direct child keeps the parent's
* line names
* @tokens --ui-layout-* (@layer variables); band widths read --layout-* (_breakpoints.css)
* @uses container-type: inline-size on each child (not nested layouts, subgrids,
* or content-sized children), so breakpoint tiers inside a band follow it
* @presets data-layout-size="sm | md | lg | xl | 2xl | full | bleed"
* (the default band for this layout's children; xl when absent)
*
* The layout element itself is the grid; nothing styles a parent. Each side band
* is a clamp() that stays at 0 until the inner content has reached its breakpoint
* width, then grows up to its fixed step, so a band caps and centers while there
* is room and fluidly fills (minus gutters) once there is not. Band widths equal
* the breakpoint widths; `layout-full` is the layout minus the gutters;
* `layout-bleed` runs edge to edge. Children are placed by the chain
* `var(--_band-resolved, var(--ui-layout-band))`: a `--band` utility (or tier) on the
* child, else the layout's default band. The `--band` no-base rule excludes layout
* children (utilities.ts), so a tier-only `--band--lg` keeps the band below lg.
*/
@layer variables {
:root {
--ui-layout-gutters: var(--gutters);
/* inline space left for the bands once the two gutters are removed */
--ui-layout-available: calc(100% - var(--ui-layout-gutters) * 2);
/* the default band for children; data-layout-size changes it per layout */
--ui-layout-band: layout-xl;
/* each child's container type: inline-size so tiers inside a band follow it;
set normal on a layout to opt its children out */
--ui-layout-child-container: inline-size;
--ui-layout-template-columns: [layout-bleed-start] var(--ui-layout-gutters) [layout-full-start]
minmax(0, 1fr) [layout-2xl-start]
clamp(
0rem,
calc((var(--ui-layout-available) - var(--layout-xl)) / 2),
calc((var(--layout-2xl) - var(--layout-xl)) / 2)
)
[layout-xl-start]
clamp(
0rem,
calc((var(--ui-layout-available) - var(--layout-lg)) / 2),
calc((var(--layout-xl) - var(--layout-lg)) / 2)
)
[layout-lg-start]
clamp(
0rem,
calc((var(--ui-layout-available) - var(--layout-md)) / 2),
calc((var(--layout-lg) - var(--layout-md)) / 2)
)
[layout-md-start]
clamp(
0rem,
calc((var(--ui-layout-available) - var(--layout-sm)) / 2),
calc((var(--layout-md) - var(--layout-sm)) / 2)
)
[layout-sm-start] clamp(0rem, var(--ui-layout-available), var(--layout-sm)) [layout-sm-end]
clamp(
0rem,
calc((var(--ui-layout-available) - var(--layout-sm)) / 2),
calc((var(--layout-md) - var(--layout-sm)) / 2)
)
[layout-md-end]
clamp(
0rem,
calc((var(--ui-layout-available) - var(--layout-md)) / 2),
calc((var(--layout-lg) - var(--layout-md)) / 2)
)
[layout-lg-end]
clamp(
0rem,
calc((var(--ui-layout-available) - var(--layout-lg)) / 2),
calc((var(--layout-xl) - var(--layout-lg)) / 2)
)
[layout-xl-end]
clamp(
0rem,
calc((var(--ui-layout-available) - var(--layout-xl)) / 2),
calc((var(--layout-2xl) - var(--layout-xl)) / 2)
)
[layout-2xl-end] minmax(0, 1fr) [layout-full-end] var(--ui-layout-gutters) [layout-bleed-end];
}
}
@layer ui.components {
:where(ui-layout, [data-ui~="layout"]) {
display: grid;
grid-template-columns: var(--ui-layout-template-columns);
/* the bands are positioned exactly by the template: no column gap */
column-gap: 0;
/* children sit at the block start and stretch across their band */
place-items: start stretch;
/* a container context would break a nested subgrid */
container-type: normal;
}
:where(ui-layout, [data-ui~="layout"]) > * {
grid-column: var(--_band-resolved, var(--ui-layout-band));
}
/* Each child is an inline-size query container, so breakpoint tiers inside
a band follow the band's width, not the full-width section around it.
Containment would stop a subgrid (a nested layout, or a child aligning to
the bands) and collapse a child sized by its content, so those stay out. */
:where(ui-layout, [data-ui~="layout"])
> :where(
:not(
ui-layout,
[data-ui~="layout"],
[style*="subgrid"],
[style*="fit-content"],
[style*="min-content"],
[style*="max-content"]
)
) {
container-type: var(--ui-layout-child-container);
}
/* A layout placed directly in a band is a subgrid and keeps the parent's line
names; one deeper inside (in a card, a section) has no parent grid to share,
so it stays its own band grid */
:where(ui-layout, [data-ui~="layout"]) > :where(ui-layout, [data-ui~="layout"]) {
grid-template-columns: subgrid;
}
:where(ui-layout[data-layout-size="sm"], [data-ui~="layout"][data-layout-size="sm"]) {
--ui-layout-band: layout-sm;
}
:where(ui-layout[data-layout-size="md"], [data-ui~="layout"][data-layout-size="md"]) {
--ui-layout-band: layout-md;
}
:where(ui-layout[data-layout-size="lg"], [data-ui~="layout"][data-layout-size="lg"]) {
--ui-layout-band: layout-lg;
}
:where(ui-layout[data-layout-size="xl"], [data-ui~="layout"][data-layout-size="xl"]) {
--ui-layout-band: layout-xl;
}
:where(ui-layout[data-layout-size="2xl"], [data-ui~="layout"][data-layout-size="2xl"]) {
--ui-layout-band: layout-2xl;
}
:where(ui-layout[data-layout-size="full"], [data-ui~="layout"][data-layout-size="full"]) {
--ui-layout-band: layout-full;
}
:where(ui-layout[data-layout-size="bleed"], [data-ui~="layout"][data-layout-size="bleed"]) {
--ui-layout-band: layout-bleed;
}
}Bands
Bands share names with the breakpoints but are a separate rem scale, --layout-sm to --layout-2xl (40, 48, 64, 80, 96rem). Two more bands span wider: layout-full fills the layout minus the gutters, and layout-bleed runs edge to edge.
- Change the default for every child with
data-layout-size="md". - Place one child with
--band: layout-md; respond with--band--lg: layout-xl. - A layout nested in a layout is a subgrid: it keeps the parent's band lines, so a bleed section can still align its content to the page bands.
Children are containers
Each child of a layout is an inline-size query container, so breakpoint tiers inside a band follow the band's width rather than the full-width section around it. Nested layouts, subgridded children, and content-sized children (fit-content, min-content) stay uncontained. The hook --ui-layout-child-container: normal opts a layout's children out.
The gutters come from --gutters (default --space-md). Hooks are listed on the layout page.