The contract

Everything the scaffold guarantees, on one page: 14 slots, one universal paint rule, four axes, one rule about layers that explains most of what goes wrong, seven layout utilities, and an optional set of reference components to copy from.

The model

TOKENS      --canvas --ink --surface --line, --primary/--primary-on …   theme sets these
SLOTS       --bg --fg --pad --gap --radius --font-size …                library, fixed
AXES        tone · variant · size · state                               library, fixed
PAINT       one universal rule                                          library, fixed
─────────────────────────────────────────────────────────────────────────────────────
UTILITIES   stack row cluster center grid switcher sidebar              library, fixed
─────────────────────────────────────────────────────────────────────────────────────
COMPONENTS  whatever this project needs                                 ← YOU WRITE THESE

Only the last row grows. A component is about six lines because everything above it is already solved. The tones, the variants, the sizes, the states and both themes are supplied by the rows it never mentions.

The slots

A slot splits deciding a value from applying it. Plain CSS does both in one rule, so every change after that needs another rule that knows the component. Here a component only fills blanks, --bg: var(--tone-soft), and one shared rule applies them. A rule like [data-variant="outline"] { --bg: transparent } knows nothing about any component, so it works on all of them, including ones not written yet. The cost of the system drops from axes times components to axes plus components.

Every component is painted from these slots and only these. All are registered inherits: false with no initial-value, so a component's background cannot cascade onto its children, and an unset slot stays guaranteed-invalid. That is what makes var(--pad, revert-layer) hand the property back to the UA stylesheet.

--bgregistered inherits: false, no initial-value
--fgregistered inherits: false, no initial-value
--border-colorregistered inherits: false, no initial-value
--border-widthregistered inherits: false, no initial-value
--border-styleregistered inherits: false, no initial-value
--radiusregistered inherits: false, no initial-value
--padregistered inherits: false, no initial-value
--gapregistered inherits: false, no initial-value
--font-sizeregistered inherits: false, no initial-value
--weightregistered inherits: false, no initial-value
--line-heightregistered inherits: false, no initial-value
--letter-spacingregistered inherits: false, no initial-value
--shadowregistered inherits: false, no initial-value
--transitionregistered inherits: false, no initial-value
@property --bg { syntax: "*"; inherits: false; }
@property --fg { syntax: "*"; inherits: false; }
@property --border-color { syntax: "*"; inherits: false; }
@property --border-width { syntax: "*"; inherits: false; }
@property --border-style { syntax: "*"; inherits: false; }
@property --radius { syntax: "*"; inherits: false; }
@property --pad { syntax: "*"; inherits: false; }
@property --gap { syntax: "*"; inherits: false; }
@property --font-size { syntax: "*"; inherits: false; }
@property --weight { syntax: "*"; inherits: false; }
@property --line-height { syntax: "*"; inherits: false; }
@property --letter-spacing { syntax: "*"; inherits: false; }
@property --shadow { syntax: "*"; inherits: false; }
@property --transition { syntax: "*"; inherits: false; }
@property --scale { syntax: "<number>"; inherits: true; initial-value: 1; }

--scale is the only registered inheriting property: it is declared @property --scale { syntax: "<number>"; inherits: true; initial-value: 1 }, so it is type-checked and animatable. --tone and its derivations are not registered at all. They inherit because inheritance is the default for a custom property. The distinction matters: only a registered property is checked against a syntax, only a registered property can be transitioned, and only a registered universal property with no initial value becomes guaranteed-invalid when unset.

--scaleregistered inherits: true, type-checked and animatable
--tonenot registered, inherits because that is the default
--tone-contrastnot registered, inherits because that is the default
--tone-inknot registered, inherits because that is the default
--tone-linenot registered, inherits because that is the default
--tone-softnot registered, inherits because that is the default

The paint rule

* {
  color: var(--fg, revert-layer);
  background-color: var(--bg, revert-layer);
  border-width: var(--border-width, revert-layer);
  border-style: var(--border-style, revert-layer);
  border-color: var(--border-color, revert-layer);
  border-radius: var(--radius, revert-layer);
  padding: var(--pad, revert-layer);
  gap: var(--gap, revert-layer);
  font-size: var(--font-size, revert-layer);
  font-weight: var(--weight, revert-layer);
  line-height: var(--line-height, revert-layer);
  letter-spacing: var(--letter-spacing, revert-layer);
  box-shadow: var(--shadow, revert-layer);
  transition: var(--transition, revert-layer);
}

One rule reads the slots and applies them to every element. It is safe universally only because an unset slot is guaranteed-invalid, so var(…, revert-layer) fires and hands the property straight back to the UA stylesheet. A <ul> keeps its indent and an <h1> its size. The selector is a bare *, not :where(*): the universal selector already contributes no specificity, so there is nothing to wrap. Note background-color, not the background shorthand, which would also reset background-image and the rest of the family.

Universal is also what keeps generated CSS uniform. Because every element is paintable the same way, "is this element paintable?" is never a question you have to answer, and a question you do not answer is one you cannot answer differently twice. Narrow the rule and it becomes a real decision, slots here and plain CSS there, made fresh for every component; that choice point is where drift enters. The closed vocabulary depends on it too: CSS has hundreds of properties and this algebra has fourteen slots, and an element outside the rule is an element where the only option left is arbitrary CSS. Uniformity then follows by construction rather than by discipline. Every painted surface goes through the same slots, so tone, variant, size and theme reach all of it. A narrowed rule would sort elements into two tiers and produce pages where some parts follow the theme and some quietly do not. It is also nothing to remember. An allowlist would have to be carried in this contract and consulted correctly every time, while * costs no tokens and cannot be misremembered.

The axes

Four axes. A component mentions none of them and gets all of them.

tone

attributedata-tone
inheritsyes
valuesprimary · secondary · success · info · warning · danger · neutral

Tone inherits, and that is the point rather than a convenience: <section data-tone="danger"> re-tones every component inside it with no per-child markup. A component never names a colour; it names var(--tone) and its derivations, and the theme decides what that resolves to.

variant

attributedata-variant
inheritsno
valuessolid · soft · outline · ghost

Derived from the tone, never a separate colour. It deliberately does NOT inherit: with a universal paint rule there is no marker separating a component from a bare <span>, so a subtree variant would paint every wrapper element it passed through.

This axis is optional, and that is worth saying because the other three are not. The four variants are ways of applying a tone. If a component's variations are a surface treatment rather than a tone, say a hairline in --line over --canvas, routing them through data-variant will tint the border and the label. Write classes instead. Tone, size and state still apply; only this one has a premise your component can fail to meet.

size

attributedata-size
inheritsyes
valuesxs · sm · md · lg · xl

A single --scale multiplier, not a set of per-size rules. Multiply by var(--scale) where you set a size and express everything else in em, and padding follows type for free. This is why a component never needs a size variant of its own.

state

attributenone, it comes from the DOM
inheritsfrom the DOM
values:disabled · :user-invalid · :read-only · :focus-visible

State comes from the DOM and from real HTML attributes. It is not settable through the generative-UI manifest and should never be simulated with a class, because the browser already knows the answer.

Layer order

largen.fallback
  < largen.reset
  < largen.tokens
  < largen.paint
  < largen.tone
  < largen.elements
  < largen.components
  < largen.modifiers

elements sits before components so a component class beats a bare-element default; modifiers sits last so an explicit variant beats a component's own default fill. Consumer CSS always wins without !important because unlayered author CSS outranks every layer, and because the element and component selectors are :where()-wrapped to contribute no specificity. The paint rule is the one that is not wrapped, because a bare * is already specificity-free. fallback sorts below everything and exists only for engines that predate @property (Firefox 113–127, Safari 16.2–16.3): behind an engine-sniff @supports, it re-declares every slot to initial per element so a slot cannot inherit; in a conforming engine it matches nothing and the registrations are authoritative.

Layout utilities

Seven utilities, configured by custom properties the same way components are. --gap is a slot, so the paint rule applies it. Responsiveness is intrinsic: switcher and sidebar reflow on the CONTAINER, so there are no breakpoints and no sm: / xl: variants to learn.

stackvertical flow. Reads --gap (1rem)
rowhorizontal, does not wrap. Reads --gap (1rem)
clusterhorizontal, wraps: tags, icon groups, metadata. Reads --gap (0.5rem)
centerconstrained measure, centred. Reads --measure (56rem), --pad
gridas many columns as fit, no breakpoints. Reads --gap (1rem), --min-item (16rem)
switchera row that becomes a stack below a container width. Reads --gap (1rem), --threshold (24rem)
sidebarside column plus fluid main, collapses when main gets too narrow. Reads --gap (2rem), --side (16rem), --min-content (50%)

Alignment is attributes, not classes: data-align="start|center|end|baseline|stretch" and data-justify="start|center|end|between". They are orthogonal, so the utilities take two attributes instead of a class per combination. Reach for these before writing align-items by hand.

Do not hand-write display: flex with align-items and gap. That is row or cluster plus data-align, and the hand-written version leaves the algebra. --gap set as plain gap is no longer a slot, so nothing can reach it.

Reference components

32 optional components, each about six lines. Copy-in rather than imported: largen ships a scaffold, not a dependency, so take the source and it is yours to edit.

There is no button here, and no input, select or table. Those are elements, and src/elements.css already themes them. They answer to data-tone, data-variant and data-size exactly like everything below. A component class duplicating them would be a worse copy of something the platform provides.

Each example below is rendered from a validated spec by the same validator and renderer the MCP server uses, so nothing on this page is something validate_spec would reject. Fetch any source with get_component_source, or read reference.css whole. Entries marked fragment are meant to sit inside a parent; shown alone they are a piece, not a demonstration.

Feedback

alert A message reporting a state. Use with a tone.
Your export finished.
.alert, l-alert {
    --bg: var(--tone-soft);
    --fg: var(--tone-ink);
    --border-color: var(--tone-line);
    --border-width: var(--hairline);
    --border-style: solid;
    --radius: var(--radius-md);
    --pad: 0.85em 1em;
    --gap: 0.75em;
    display: grid;
    grid-template-columns: auto 1fr auto;
    align-items: center;
}
badge A short inline label or count.
beta
.badge, l-badge {
    --bg: var(--tone-soft);
    --fg: var(--tone-ink);
    --border-color: var(--tone-line);
    --border-width: var(--hairline);
    --border-style: solid;
    --radius: 2em;
    --pad: 0.15em 0.6em;
    --gap: 0.3em;
    --font-size: calc(0.78rem * var(--scale));
    --weight: var(--weight-medium);
    display: inline-flex;
    align-items: center;
    inline-size: fit-content;
    white-space: nowrap;
}
dot A bare status dot. No content.
.dot, l-dot {
    --bg: var(--tone);
    --radius: 50%;
    display: inline-block;
    inline-size: calc(0.55em * var(--scale));
    block-size: calc(0.55em * var(--scale));
    vertical-align: middle;
}
spinner An indeterminate activity indicator.
.spinner, l-spinner {
    --fg: var(--tone);
    display: inline-block;
    inline-size: calc(1.15em * var(--scale));
    block-size: calc(1.15em * var(--scale));
    border: 2px solid currentcolor;
    border-block-start-color: transparent;
    border-radius: 50%;
    animation: largen-spin 0.6s linear infinite;
}

.spinner, l-spinner, .skeleton, l-skeleton {
      animation-duration: 0.01ms;
      animation-iteration-count: 1;
}
skeleton A loading placeholder occupying a future element's box.
.skeleton, l-skeleton {
    --bg: var(--surface);
    --radius: var(--radius-sm);
    display: block;
    min-block-size: 1em;
    background-image: linear-gradient(90deg, transparent,
      color-mix(in oklab, var(--ink) 6%, transparent), transparent);
    background-size: 200% 100%;
    animation: largen-sweep 1.4s linear infinite;
}

.spinner, l-spinner, .skeleton, l-skeleton {
      animation-duration: 0.01ms;
      animation-iteration-count: 1;
}

Surfaces

card A container grouping related content on a surface.
Storage used
48.6 GB
.card, l-card {
    --bg: var(--surface);
    --fg: var(--ink);
    --border-color: var(--line);
    --border-width: var(--hairline);
    --border-style: solid;
    --radius: var(--radius-md);
    --pad: 1rem;
    --gap: 0.5rem;
    display: flex;
    flex-direction: column;
}
panel A surface with no border.
panel content
.panel, l-panel {
    --bg: var(--surface);
    --radius: var(--radius-md);
    --pad: 1rem;
    --gap: 0.75rem;
    display: flex;
    flex-direction: column;
}
divider A labelled or bare horizontal rule.
or
.divider, l-divider {
    --fg: var(--ink-muted);
    --gap: 0.75em;
    --font-size: calc(0.8rem * var(--scale));
    display: flex;
    align-items: center;
}

.divider::before, .divider::after, l-divider::before, l-divider::after {
    content: "";
    flex: 1;
    border-block-start: var(--hairline) solid var(--line);
}

.divider::before, .divider::after, l-divider::before, l-divider::after {
    content: "";
    flex: 1;
    border-block-start: var(--hairline) solid var(--line);
}

Data

stat A figure with a label. Wraps stat-label and stat-value.
Requests today
18,204
.stat, l-stat {
    --gap: 0.15em;
    display: flex;
    flex-direction: column;
}
stat-label The label above a figure.
Requests today
.stat-label, l-stat-label {
    --fg: var(--ink-muted);
    --font-size: calc(0.72rem * var(--scale));
    --letter-spacing: 0.06em;
    text-transform: uppercase;
}
stat-value The figure itself. Renders tabular-nums.
18,204
.stat-value, l-stat-value {
    --fg: var(--tone-ink);
    --font-size: calc(1.75rem * var(--scale));
    --weight: var(--weight-bold);
    font-variant-numeric: tabular-nums;
    line-height: 1.1;
}

Forms

field Groups a label, a control and its hint or error.
Lowercase letters and hyphens.
.field, l-field {
    --gap: 0.35em;
    display: flex;
    flex-direction: column;
}
field-label The label above a form control.
.field-label, l-field-label {
    --font-size: calc(0.875rem * var(--scale));
    --weight: var(--weight-medium);
    display: block;
}
field-hint Guidance under a form control.
Lowercase letters and hyphens.
.field-hint, l-field-hint {
    --fg: var(--ink-muted);
    --font-size: calc(0.8125rem * var(--scale));
    display: block;
}
field-error A validation message under a form control. Carries the danger tone itself.
That name is already taken.
.field-error, l-field-error {
    --tone: var(--danger);
    --fg: var(--tone-ink);
    --font-size: calc(0.8125rem * var(--scale));
    display: block;
}

Containers

table-wrap A scroll container for a wide table. The table itself is already themed.
a wide table goes here
.table-wrap, l-table-wrap {
    --border-color: var(--line);
    --border-width: var(--hairline);
    --border-style: solid;
    --radius: var(--radius-md);
    display: block;
    overflow-x: auto;
    max-inline-size: 100%;
}
toolbar A wrapping row of controls.
All Open Closed
.toolbar, l-toolbar {
    --gap: 0.5em;
    --pad: 0.5em;
    display: flex;
    flex-wrap: wrap;
    align-items: center;
}

Empty state

empty An empty state. Wraps empty-title and empty-note.
No deployments yet
When you ship something, it will show up here.
.empty, l-empty {
    --gap: 0.4em;
    --pad: 2.5em 1.5em;
    --fg: var(--ink-muted);
    display: flex;
    flex-direction: column;
    align-items: center;
    text-align: center;
}
empty-title The headline of an empty state.
No deployments yet
.empty-title, l-empty-title {
    --fg: var(--ink);
    --weight: var(--weight-medium);
    --font-size: calc(1rem * var(--scale));
    display: block;
}
empty-note The explanation under an empty state's headline.
When you ship something, it will show up here.
.empty-note, l-empty-note {
    --font-size: calc(0.875rem * var(--scale));
    display: block;
    max-inline-size: 40ch;
}

Navigation

crumbs An ancestry trail.
    .crumbs, l-crumbs {
        --fg: var(--ink-muted);
        --gap: 0.5em;
        --font-size: calc(0.85rem * var(--scale));
        display: flex;
        flex-wrap: wrap;
        align-items: center;
        list-style: none;
        margin: 0;
        --pad: 0;
    }
    steps A numbered sequence. Mark position with aria-current.
      .steps, l-steps {
          --gap: 0;
          display: flex;
          counter-reset: largen-step;
          list-style: none;
          margin: 0;
          --pad: 0;
      }

      Conversation

      bubble A conversation bubble. data-side="end" for the user's own turn.
      Is the deploy finished?
      .bubble, l-bubble {
          --bg: var(--surface);
          --fg: var(--ink);
          --radius: var(--radius-lg);
          --pad: 0.5em 0.8em;
          display: block;
          inline-size: fit-content;
          max-inline-size: 46ch;
      }
      
      .bubble[data-side="end"], l-bubble[data-side="end"] {
          --bg: var(--tone);
          --fg: var(--tone-contrast);
          margin-inline-start: auto;
      }
      avatar A person's image or initials.
      VR
      .avatar, l-avatar {
          --bg: var(--surface);
          --fg: var(--ink-muted);
          --radius: 50%;
          --font-size: calc(0.75rem * var(--scale));
          display: inline-grid;
          place-items: center;
          inline-size: calc(2.25rem * var(--scale));
          block-size: calc(2.25rem * var(--scale));
          overflow: hidden;
          vertical-align: middle;
      }

      Overlay

      tip Wraps a child; hint text goes in data-tip.
      hover me
      .tip, l-tip {
          position: relative;
          display: inline-block;
      }
      
      .tip::after, l-tip::after {
          content: attr(data-tip);
          position: absolute;
          inset-block-end: 100%;
          inset-inline-start: 50%;
          translate: -50% -0.35em;
          padding: 0.35em 0.55em;
          border-radius: var(--radius-sm);
          background-color: var(--ink);
          color: var(--canvas);
          font-size: 0.75rem;
          white-space: nowrap;
          opacity: 0;
          pointer-events: none;
          transition: opacity var(--speed);
      }
      
      .tip:is(:hover, :focus-visible)::after, l-tip:is(:hover, :focus-visible)::after { opacity: 1
      }

      Text

      prose Long-form rendered text. Wrap markdown output in this.
      Long-form text, with its own vertical rhythm.
      .prose, l-prose {
          --fg: var(--ink);
          --measure: 68ch;
          --flow: 1.25em;
          --font-size: calc(1rem * var(--scale));
          line-height: 1.7;
          max-inline-size: var(--measure);
          text-wrap: pretty;
      }

      Layout utilities

      stack Vertical layout. Set --gap.
      first second third
      .stack {
          --gap: 1rem;
          display: flex;
          flex-direction: column;
      }
      row Horizontal layout, no wrap.
      left right
      .row {
          --gap: 1rem;
          display: flex;
          align-items: center;
      }
      cluster Horizontal layout that wraps. Tag lists, icon groups.
      css design largen
      .cluster {
          --gap: 0.5rem;
          display: flex;
          flex-wrap: wrap;
          align-items: center;
      }
      grid As many columns as fit at --min-item.
      one
      two
      three
      .grid {
          --gap: 1rem;
          --min-item: 16rem;
          display: grid;
          grid-template-columns: repeat(auto-fill, minmax(min(var(--min-item), 100%), 1fr));
      }
      center Constrained measure, centred.
      centred, at a measure
      .center {
          --measure: 56rem;
          --pad: 0 1rem;
          inline-size: 100%;
          max-inline-size: var(--measure);
          margin-inline: auto;
      }

      Using them

      <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/largen@latest/dist/largen.css">
      <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/largen@latest/dist/largen.components.css">

      Or copy one component's source and skip the file entirely. That is the intended path. The set exists to be read and taken from, not depended on.