Design System Contracts

Spec reference

Layout

Flexbox and auto-layout are the same declaration here; display:grid is the third spelling — plus per-variant overrides for the cases where a value legitimately deviates.

The layout blockGeneratedCurated#

One vocabulary, two projections: flexbox on the code side, auto-layout on the canvas; display: grid is the third spelling (declared-track grids — see Grid). The properties are the intersection both surfaces can honor — that is the point.

display"flex" | "inline-flex" | "grid"

flex, inline-flex, or grid (code). Flex spellings become auto-layout on the canvas; grid becomes a GRID frame with declared tracks.

direction"row" | "column"

Row or column. Reversed directions exist only as per-variant overrides (see below) — the canvas has no reverse, so they are compiled away. Flex-only — schema-invalid with display: grid.

align"start" | "center" | "end" | "stretch" | "baseline"

Cross-axis alignment. Flex-only — schema-invalid with display: grid (per-cell alignment lives on placement).

justify"start" | "center" | "end" | "space-between"

Main-axis distribution. Flex-only — schema-invalid with display: grid.

growboolean

The part takes remaining space — code: flex: 1 1 auto; canvas: fill container.

overlapboolean

Children overlap (AvatarGroup): the gap token is applied as a negative child margin in CSS and as negative item spacing on the canvas. Flex-only.

wrapboolean

v15: children wrap (tag groups, chip rows) — code: flex-wrap: wrap; canvas: native layoutWrap: WRAP. Flex-only.

rowsArray<{ px: number } | { fr: number } | { fit: true }>

G1: declared row track list. Each track is exactly one of {px}, {fr}, or {fit: true} — the three spellings the Plugin API round-trips (FIXED / FLEX / HUG). Required on a grid unless flow: "row" lets the emitter derive them.

columnsArray<{ px: number } | { fr: number } | { fit: true }>

G1: declared column track list — required on every display: grid. Same track spellings as rows.

gap{ row: number | TokenRef; column: number | TokenRef }

G1: independent row / column gaps (px or token refs). There is no single-value shorthand — proposers normalize CSS gap into the pair.

areasRecord<string, { row: number; column: number; rowSpan?: number; columnSpan?: number }>

G4: named areas as slot anchors. The key is simultaneously a slot name and a placement rect (row/column/spans). A part with the same name takes the area; declaring both an area and an explicit placement for one name is schema-invalid.

flow"row"

G5: the one bounded auto-flow the canvas has — exactly "row". Placement fact is child order. column and dense refuse by name.

contracts/avatar-group.contract.json (excerpt · v1.0.0) — shipping contract, loaded at build time · overlap — the negative-spacing projection
{
  "anatomy": {
    "root": {
      "parts": {
        "stack": {
          "layout": {
            "direction": "row",
            "align": "center",
            "overlap": true
          },
          "tokens": {
            "gap": "{space.avatarGroup.overlap}"
          }
        }
      }
    }
  }
}

Grid — declared tracks, areas, flowGeneratedCurated#

A2 grid is a first-class layout mode, not a flex fallback. display: "grid" requires a declared columns track list; rows are required unless flow: "row" lets the emitter derive them. Flex facts (direction, align, justify, wrap, overlap) are schema-invalid on a grid. Both surfaces carry the same tracks, gap pair, and cell rects — Figma has no native area names, so the contract owns them.

Refusals: Each of these fails the build by name — the generator refuses, it never papers over (source: core/emit-react.ts validateContract, exercised by the C2 eval family).

  • display: grid without columns
  • flex-only fields together with display: grid
  • flow: "row" together with areas — a grid declares areas or flow, never both
  • track spellings outside {px} / {fr} / {fit: true} (percent, minmax, repeat, zero, negative)
  • flow values other than "row" (column, dense)
contracts/bento-grid.contract.json (excerpt · v1.0.0) — shipping contract, loaded at build time · declared tracks + named areas (G1/G4)
{
  "anatomy": {
    "root": {
      "layout": {
        "display": "grid",
        "rows": [
          {
            "px": 80
          },
          {
            "fr": 1
          },
          {
            "fr": 2
          }
        ],
        "columns": [
          {
            "px": 160
          },
          {
            "fr": 1
          },
          {
            "fr": 1
          },
          {
            "px": 120
          }
        ],
        "gap": {
          "row": "{space.gap.sm}",
          "column": "{space.gap.md}"
        },
        "areas": {
          "header": {
            "row": 0,
            "column": 0,
            "columnSpan": 4
          },
          "sidebar": {
            "row": 1,
            "column": 0,
            "rowSpan": 2
          },
          "main": {
            "row": 1,
            "column": 1,
            "columnSpan": 2
          },
          "rail": {
            "row": 1,
            "column": 3,
            "rowSpan": 2
          },
          "footer": {
            "row": 2,
            "column": 1,
            "columnSpan": 2
          }
        }
      }
    }
  }
}

Grid cell placementGeneratedCurated#

part.placement is legal only on a child whose parent declares layout.display: "grid". Anchors are 0-based; spans default to 1. Alignment vocabulary is auto | start | center | endstretch is spelled as fill sizing, baseline refuses by name. A part named by layout.areas must not also carry an explicit placement (the area name is the anchor).

rowrequirednumber

0-based row anchor. Negative indexes refuse by name.

columnrequirednumber

0-based column anchor.

rowSpannumber

Row span, default 1. Must stay inside the declared track list.

columnSpannumber

Column span, default 1.

alignX"auto" | "start" | "center" | "end"

In-cell horizontal alignment: auto / start / center / end.

alignY"auto" | "start" | "center" | "end"

In-cell vertical alignment: auto / start / center / end.

Refusals: Each of these fails the build by name — the generator refuses, it never papers over (source: core/emit-react.ts validateContract, exercised by the C2 eval family).

  • placement on a part whose parent is not display: grid
  • placement together with a same-name area (one source of truth)
  • placement on an out-of-flow / overlay / slot-wrapper part
  • occupancy collision or a span that walks off the declared tracks

hugsBelowMaxWidth — measured sizing evidenceCurated#

hugsBelowMaxWidth: boolean on a part answers one question about that part's max-width channel: is this a ceiling the box normally sits under, or the width the box is actually drawn at? A CSS max-width alone cannot tell you, and the two answers lower to opposite things on the canvas — hug-contents versus a fixed width.

The flag is measured, never assumed. The computed capture sets it to true only when the used width stayed strictly below the cap in every enumerated combination, which means the box hugs and the cap is a ceiling. A width equal to the cap means the box is sitting at it and the value may be a genuine design width, so the flag is not set and the fixed-width lowering stands. A contract with no captured evidence omits the field and keeps the design-width lowering — which is why hand-authored contracts are unaffected by its existence.

Refusals: Each of these fails the build by name — the generator refuses, it never papers over (source: core/emit-react.ts validateContract, exercised by the C2 eval family).

  • a part carrying hugsBelowMaxWidth with no max-width channel — the flag qualifies that channel and qualifies nothing else

Layout by propGeneratedCurated#

layoutByProp: { prop, map } applies per-enum-value layout overrides merged over the base layout. Partial coverage is the point — only the values that deviate appear. ChatMessage: sender=user flips direction: row-reverse on the root, right-aligning user messages.

Projections: code emits the override under the root’s enum class (.sender-user .body { … }); the canvas — which has no reverse — resolves it per variant at compile time, rendering the same children in reversed order.

layoutByProp.map values — VariantLayout, rendered from the schema
map values: VariantLayout

Refusals: Each of these fails the build by name — the generator refuses, it never papers over (source: core/emit-react.ts validateContract, exercised by the C2 eval family).

  • the driving prop must be a declared enum; every map key one of its values
  • a component-instance part refuses overrides — the child contract owns its layout
  • grow and overlap stay per-part invariants: not overridable per variant
contracts/chat-message.contract.json (excerpt · v1.1.0) — shipping contract, loaded at build time
{
  "anatomy": {
    "root": {
      "layoutByProp": {
        "prop": "sender",
        "map": {
          "user": {
            "direction": "row-reverse"
          }
        }
      }
    }
  }
}