refract v0.1 · beta
Subsystems / Layout

Layout

The structural layer — a spacing scale plus generators for columns, grids, stacks and resizable query containers.

raw slice the input
live render ↔ resize the box
refracted: the grid is the emitted dt-layout-grids-grid-cards class; the container is dt-layout-container-container. Drag its right edge to watch the layout respond.

Overview

The layout subsystem is the only closed subsystem — its keys are fixed. It has three kinds of member: regular property tokens (spacing, gutters, aspectRatio, sizes) that emit :root variables; four structural generators (columns, grids, stacks, container) that emit utility classes; and recipes that compose spacing and the sizes scale into classes.

Properties

Regular tokens — a base plus named variants, emitted as :root variables. The structural generators and recipes reference these by variant name.

PropertyValue typeEmitted variableDescription
spacingnumber | string--dt-layout-spacing-<v>The spacing scale (numbers → px). Referenced by padding/margin/gap and gutters.
guttersnumber | string--dt-layout-gutters-<v>Gutter widths for columns / container.
aspectRatiostring--dt-layout-aspectratio-<v>Aspect ratios (e.g. "16/9").
sizesnumber | string--dt-layout-sizes-<v>The sizing scale (§22) — one length scale for width / height / min / max, consumed by a recipe's sizing verbs and by a container's maxWidth.
layout.properties.ts
spacing:     { base: 8, variants: { none: 0, sm: 8, md: 12, lg: 18, xl: 28 } },
gutters:     { base: 16, variants: { compact: 10, relaxed: 32 } },
aspectRatio: { base: "auto", variants: { square: "1", video: "16/9", wide: "21/9" } },
sizes:       { base: "40rem", variants: { sm: "24rem", md: "32rem", lg: "48rem", prose: "65ch" } },

Synthesized scales

spacing, gutters and sizes can generate their variant ramp from a base and a curve — the same idea as colours' tonal steps and typography's modular scale, so you declare the shape once instead of hand-listing every rung. It's opt-in and additive: with no curve key, output is byte-identical to a hand-listed scale.

Declare exactly one curve. Geometric — a ratio; steps is an ordered name array whose index is the exponent (base × ratioindex). Linear — a step; steps is a name → multiplier map (step × multiplier). Declaring both ratio and step is an error. steps is optional — the default ladder is xs sm md lg xl 2xl 3xl 4xl (geometric: index = exponent; linear: multiplier = index + 1). Precision is fixed at 4 decimals.

CurveKeysteps shapeFormula
geometricratioordered name array (index = exponent)round(base × ratioindex, 4)
linearstepname → multiplier mapround(step × multiplier, 4)
geometric — spacing
spacing: { base: 8, ratio: 1.5, steps: ["xs", "sm", "md", "lg", "xl"] }
// → xs 8 · sm 12 · md 18 · lg 27 · xl 40.5  (base × 1.5^index; 40.5 kept, not rounded to int)
linear — spacing grid
spacing: { base: 4, step: 4, steps: { xs: 1, sm: 2, md: 3, lg: 4, xl: 6 } }
// → xs 4 · sm 8 · md 12 · lg 16 · xl 24  (step × multiplier — a 4pt grid)
emitted (default prefix)
--dt-layout-spacing-xs: 8px;
--dt-layout-spacing-md: 18px;
--dt-layout-spacing-xl: 40.5px;
--dt-layout-spacing-none: 0px;   /* the forced none stays on spacing / gutters */

Each rung is stored as a derived Ref (fn: "scaleStep") — like colours' tonal steps, not a frozen literal — so override() of the base re-synthesizes the whole ramp (override base: 16 on the geometric example and md becomes 36). A hand-authored variants entry always wins over the generated rung of the same name.

sizes coexistence. A synthesized t-shirt ramp lives alongside hand-authored semantic caps and pinned values: sizes: { base: 320, ratio: 1.5, steps: ["sm","md","lg","xl"], variants: { prose: 640, wide: 1200, full: "100%" } } gives sm 320 · md 480 · lg 720 · xl 1080 plus the literal prose 640 / wide 1200 / full 100%. A pinned % has no magnitude to multiply, so it can only ever be an authored variant — never synthesized. sizes gets no forced none. Rule of thumb: a linear grid for spacing, a geometric ramp for sizes.

Responsive ramps. A responsive entry carrying a ratio or step (plus an optional base) regenerates the whole named scale at that breakpoint — it expands into one target override per step over the existing responsive channel (no new Model shape). responsive: [{ breakpoint: "sm", query: "max", ratio: 1 }] flattens every step to the base on mobile (base × 1index); responsive: [{ breakpoint: "lg", base: 6, step: 6 }] emits a fuller ramp above lg (6 · 12 · 18 for xs:1 / sm:2 / md:3).

Structural generators

These don't emit tokens — they generate utility classes from a compact config (consuming the spacing / gutter variants above), accessible via theme.classes.layout. There are four, each documented below.

GeneratorAuthored asEmits
columns{ size, gutter?, inset? }.dt-layout-columns-col-<bp>-<1…size> · .dt-layout-columns-offset-<bp>-<n>
grids{ <name>: GridDefinition }.dt-layout-grids-grid-<name> (display: grid)
stacks{ <name>: StackDefinition }.dt-layout-stacks-stack-<name> (flex / inline-flex)
containermode string | config + variants.dt-layout-container-container · .dt-layout-container-container-<variant>

columns — the grid system

A responsive column system. size sets the column count; the generator emits a span class and an offset class for every count 1…size, at every breakpoint (the sm+ classes are wrapped in @media (min-width: …)). Children carry .dt-layout-columns-col-<bp>-N inside a CSS grid you establish (or a grid recipe). gutter / inset name gutter / spacing variants, exposed as config vars.

FieldTypeRequiredDescription
sizenumberRequiredThe column count (e.g. 12).
gutterstringOptionalNames a gutters variant → --dt-layout-columns-gutter.
insetstringOptionalNames a spacing variant → --dt-layout-columns-inset.
columns · authored
layout.columns: { size: 12, gutter: "compact", inset: "sm" }
emitted
.dt-layout-columns-col-xs-1  { grid-column-end: span 1; }
.dt-layout-columns-col-xs-6  { grid-column-end: span 6; }   /* … through span 12 */
.dt-layout-columns-offset-xs-2 { grid-column-start: 3; }
@media (min-width: 576px) {
  .dt-layout-columns-col-sm-6 { grid-column-end: span 6; }  /* the sm breakpoint set */
}

grids — named CSS grid templates

Each named grid emits a .dt-layout-grids-grid-<name> class of display: grid plus the template you author. gap names a spacing variant; a responsive list re-templates at breakpoints.

FieldTypeDescription
templateColumnsstringgrid-template-columns.
templateRowsstringgrid-template-rows.
autoColumns / autoRowsstringgrid-auto-columns / grid-auto-rows.
justifyItems / alignItemsstringItem alignment on each axis.
justifyContent / alignContentstringTrack alignment on each axis.
gapstringNames a spacing variant.
responsiveResponsiveEntry[]Per-breakpoint overrides of any field above → @media.
grids · authored
layout.grids: {
  cards:     { templateColumns: "repeat(auto-fill, minmax(140px, 1fr))", gap: "lg", alignItems: "start" },
  dashboard: { templateColumns: "repeat(4, minmax(0,1fr))", gap: "md",
               responsive: [{ breakpoint: "lg", query: "max", templateColumns: "repeat(2, minmax(0,1fr))" }] },
}
emitted
.dt-layout-grids-grid-cards {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(140px, 1fr));
  align-items: start; gap: var(--dt-layout-spacing-lg);
}
@media (max-width: 1023.98px) {
  .dt-layout-grids-grid-dashboard { grid-template-columns: repeat(2, minmax(0,1fr)); }
}

stacks — flexbox stacks

Each named stack emits a .dt-layout-stacks-stack-<name> flex container. inline: true makes it inline-flex; gap names a spacing variant; a responsive list overrides direction / align / etc. at breakpoints.

FieldTypeDescription
direction"row" | "column"flex-direction.
alignstringalign-items.
justifystringjustify-content.
wrapstringflex-wrap.
inlinebooleantrueinline-flex.
gapstringNames a spacing variant.
responsiveResponsiveEntry[]Per-breakpoint overrides → @media.
stacks · authored
layout.stacks: {
  toolbar: { direction: "row", gap: "sm", align: "center", justify: "space-between",
             responsive: [{ breakpoint: "sm", query: "max", direction: "column", align: "stretch" }] },
  tags:    { direction: "row", inline: true, wrap: "wrap", gap: "xs" },
}
emitted
.dt-layout-stacks-stack-toolbar {
  display: flex; flex-direction: row;
  align-items: center; justify-content: space-between;
  gap: var(--dt-layout-spacing-sm);
}
@media (max-width: 767.98px) {
  .dt-layout-stacks-stack-toolbar { flex-direction: column; align-items: stretch; }
}
.dt-layout-stacks-stack-tags { display: inline-flex; flex-direction: row; flex-wrap: wrap; gap: var(--dt-layout-spacing-xs); }

container — page containers

A centering wrapper. The mode (base) is "fixed" (centered, capped at maxWidth), "fluid" (full-width up to maxWidth), or a raw width string ("720px"). Named variants each emit a .dt-layout-container-container-<variant>; a responsive list overrides fields at breakpoints. inset / gutter name spacing / gutter variants → per-container config vars.

FieldTypeDescription
basestringThe mode — "fixed" / "fluid" / a width string.
insetstringHorizontal padding — names a spacing variant.
gutterstringInner gap — names a gutters variant.
maxWidthstring | numberCap width — names a sizes variant (§22), or a raw length (a number → the media unit).
direction / align / justifystringFlow of the container's own children.
variantsRecord<string, ContainerVariant>Named variants → .dt-layout-container-container-<variant>.
responsiveResponsiveEntry[]Per-breakpoint overrides (with target) → @media.
container · authored
layout.container: {
  base: "fixed", inset: "lg", gutter: "base", maxWidth: "lg",
  variants: {
    narrow: { base: "fixed", maxWidth: "md" },
    fluid:  { base: "fluid", maxWidth: 1600, inset: "xl" },
    prose:  { base: "720px" },
  },
}
emitted
.dt-layout-container-container {
  box-sizing: border-box; width: 100%;
  margin-left: auto; margin-right: auto;
  padding-left: var(--dt-layout-container-inset);
  padding-right: var(--dt-layout-container-inset);
  gap: var(--dt-layout-container-gutter);
}
.dt-layout-container-container-fluid { /* … */ max-width: 1600px; }
.dt-layout-container-container-prose { /* … */ max-width: 720px; }

Recipes — padding / section

A layout recipe names a spacing variant per prop; one prop can fan out to several declarations (paddingYpadding-top + padding-bottom). The sizing verbs (§22) name a sizes variant instead — one declaration each, no fan-out. background passes through as a literal.

Recipe propCSS declarationsValue
paddingYpadding-top · padding-bottoma spacing variant
paddingXpadding-left · padding-righta spacing variant
marginYmargin-top · margin-bottoma spacing variant
marginXmargin-left · margin-righta spacing variant
gapgapa spacing variant
width · minWidth · maxWidthwidth · min-width · max-widtha sizes variant
height · minHeight · maxHeightheight · min-height · max-heighta sizes variant
backgroundbackgrounda literal
sizing verbs → the sizes scale
// panel: { card: { maxWidth: "md", width: "lg" } }  →
.dt-layout-panel-card {
  max-width: var(--dt-layout-sizes-md);
  width: var(--dt-layout-sizes-lg);
}
A sizing verb exists because it consumes a themed scale (sizes). Dimensional CSS with no scale — display, position, overflow — stays in a component's own css delta, not here.
layout.recipes.ts
layout.recipes: {
  padding: { card: { paddingY: "lg", paddingX: "lg" }, button: { paddingY: "sm", paddingX: "lg" } },
  section: { hero: { paddingY: "3xl", paddingX: "xl", marginY: "xl", gap: "lg" } },
}
dist/theme.css · emitted
:root { --dt-layout-spacing-lg: 18px; --dt-layout-gutters-compact: 10px; }

.dt-layout-columns-col-xs-6 { grid-column-end: span 6; }
.dt-layout-grids-grid-cards {
  display: grid; grid-template-columns: repeat(auto-fill, minmax(140px, 1fr));
  align-items: start; gap: var(--dt-layout-spacing-lg);
}
.dt-layout-stacks-stack-toolbar { display: flex; flex-direction: row; align-items: center; justify-content: space-between; gap: var(--dt-layout-spacing-sm); }
.dt-layout-container-container { box-sizing: border-box; width: 100%; margin-left: auto; margin-right: auto; padding-left: var(--dt-layout-container-inset); /* … */ }
.dt-layout-padding-card { padding-top: var(--dt-layout-spacing-lg); padding-bottom: var(--dt-layout-spacing-lg); /* … */ }
Technical documentation for @theme-registry/refract · all output is compiled client-side by the real library (CSS adapter). The chrome is theme-aware; the render panes carry each preset's own world. · MIT licensed.