Layout

Layout in Milda is the propose/choose model: a container proposes room, each child says how much it wants, the container places it. It is the model SwiftUI, Compose and Flutter already are, which makes the web the target that needs translating. You write intents, never mechanics.

#Propose/choose, in plain language

Every layout negotiation is the same three-step conversation. A container tells each child how much room is on offer; the child answers with the size it wants - its content size, all of it, a share, a fixed amount, a ratio of its other axis; the container then places the children. That is the entire model. CSS flexbox can be expressed in propose/choose, but not the reverse, so the abstract model is the native one and the web generator translates. Grow and shrink factors, the min-width gotcha and the rest of flexbox's mechanics are realization details the language never exposes.

#A container and its children

milda
foundations {
  token space/sm : spacing = 8px
  token space/md : spacing = 12px
  token space/xl : spacing = 24px
  token size/sidebar : size = 280px
  token size/min : size = 160px
  token size/max : size = 720px
}

component Card {
  part Card: container {
    layout { stack column; gap space.md; inset space.xl; size fill min(size.min) max(size.max) fit }
  }
  part Header: container {
    layout { stack row; gap space.sm; align center; distribute between }
  }
  part Header.Title: text {
    content "Project Aurora"
    layout { size fill }
  }
  part Header.Status: text {
    content "Beta"
  }
  part Cover: container {
    layout { size fill ratio(16:9); position bleed inline }
  }
  part Footer: container {
    layout { stack row; gap space.sm; distribute end }
  }
}
  • makes a part a layout container, flowing children along row, column or layers (the z axis: children on top of one another, the stack sizing to the largest). A part with no is not a container: the generator emits no flex box for it.
  • is the main axis: start, center, end, between, around, evenly. is the cross axis: start, center, end, stretch, baseline. In a layers stack, distribute is horizontal and align vertical.
  • wrap starts new lines when out of room: nowrap, wrap, wrap-reverse.
  • gap and inset are tokens, like every length: gap between children, inset (padding) as one value for all edges or block then inline.

#Child sizing: an intent per axis

A child declares what it wants on each axis, width then height, and may name one axis alone: size fill fit, size height fit. Five intents replace the grow/shrink/basis triple:

  • fit - size to content. The default.
  • fill - take all the room offered. On the main axis the web keeps the siblings whole (flex: 1 1 0% with the min-width: 0 the web needs and natives do not); across it, the whole width.
  • weight(n) - a share of the leftover room against the siblings' weights.
  • fixed(size.sidebar) - exactly a token's length.
  • ratio(16:9) - this axis follows the other by the ratio: size fill ratio(16:9) is a width that fills and a height that keeps 16:9.

Any intent may be clamped: size fill min(size.min) max(size.max) fit. The axis never shrinks below the one nor grows past the other.

milda
foundations {
  token space/md : spacing = 12px
  token size/sidebar : size = 280px
}

component Shell {
  part Shell: container {
    layout { stack row; gap space.md; size fill fill }
  }
  part Sidebar: container {
    layout { size fixed(size.sidebar) fill }
  }
  part Main: container {
    layout { size weight(2) fill }
  }
  part Aside: container {
    layout { size weight(1) fill }
  }
  part Toolbar: container {
    layout { position pinned block-start }
  }
}

#Position: leaving the flow

  • flow - in the normal parent-directed flow. The default.
  • overlay - out of flow, on top of its layers stack, placed by the stack's align and distribute. How a badge sits on an avatar.
  • pinned <edge>… - stays at that edge of its scroll container: a sticky header, a toolbar.
  • bleed <edge>… - stays in flow but cancels the parent's inset on the named edges, extending the child to the parent's edge: the full-bleed cover inside a padded card.

Edges are logical: block-start, block-end, inline-start, inline-end, block, inline, all, so a layout survives a right-to-left locale. The physical words top, bottom, left and right are read as the logical edge they name left-to-right and never written.

milda
foundations {
  token space/xs : spacing = 4px
  token size/avatar : size = 40px
}

component Avatar {
  part Avatar: container {
    layout { stack layers; align end; distribute end; size fixed(size.avatar) fixed(size.avatar) }
  }
  part image: content {
    media img
    layout { size fill fill }
  }
  part badge: container {
    layout { position overlay; inset space.xs }
  }
}

#Conditional layout

A part can carry a default layout plus prop-conditioned overrides - layout when orientation = horizontal { … } - the principled home for a prop-driven structure switch, such as a data list rendering as a column or a two-track row. Values that vary by context use token aliasing instead; see Tokens & context. A or block may also carry a layout { } beside its facets.

#Three layers

The spec definesA .milda file can sayWhat each target does
The propose/choose substrate, the containers and facets above, per-axis intents with clamps, and the four positions on logical edges. A deliberately small model: a concept earns its place only if it means the same thing on every target.Every word on this page. Read for compatibility and never written: flow as a key (= wrap wrap), ratio 16 9 as a key, the physical edges, and grid cols 2, which is reserved and reported: no target draws a grid yet.Each target declares its coverage of every slot a layout can use, and that declaration is what produces its report, so behaviour and report cannot disagree. The web draws all of it (a layers stack is one grid cell, a bleed a logical negative margin, a pin position: sticky). Figma sizes and locks ratios and clamps, draws evenly as between and equal weights, and cannot express a pin or a bleed, which it reports.
Built

One layout vocabulary: stack, wrap, gap, align, distribute, inset, per-axis size with weight, fixed, ratio, min and max, and position flow, overlay, pinned and bleed on logical edges, read by the surface, held by the IR and drawn by the web and Figma targets. A grid is reserved and reported, not drawn.proposal 0056

#What deliberately stays out

  • Advanced grid tracks - fr, minmax, auto-fit are web-leaning concepts with no faithful native equivalent. grid is reserved until a second target can draw one honestly.
  • Scroll-container negotiation - nested-scroll cross-axis sizing diverges between platforms.
  • Text truncation and wrapping are not layout: they are the text.clamp and text.wrap facets.
Why so small
A layout concept earns its place only if it means the same thing on every target. Everything on this page passes that bar; what does not is named as realization work rather than papered over with a leaky abstraction.

Every word on this page is one entry in the reference; the paint side of a part is on Facets.

These words, in the reference

  • How the part lays out its children and how it sizes itself, in the propose/choose model.

Open a word for its entry, or read them all in the reference.

See it built in the tour9. Layout