Skip to content

Type to search the docs, API, and blog.

↑↓ navigate↵ openesc close
Zazz 0.7 alpha
API navigation

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.

layout-bleededge to edge
layout-fullminus the gutters
layout-2xl96rem
layout-xl80rem (default)
layout-lg64rem
layout-md48rem
layout-sm40rem
Each child of a layout sits in one band. Place it with --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.