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.
@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.
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
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
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
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
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.
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, 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, 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, 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, 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, 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, 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, l-panel {
--bg: var(--surface);
--radius: var(--radius-md);
--pad: 1rem;
--gap: 0.75rem;
display: flex;
flex-direction: column;
}
.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, l-stat {
--gap: 0.15em;
display: flex;
flex-direction: column;
}
.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, 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, l-field {
--gap: 0.35em;
display: flex;
flex-direction: column;
}
.field-label, l-field-label {
--font-size: calc(0.875rem * var(--scale));
--weight: var(--weight-medium);
display: block;
}
.field-hint, l-field-hint {
--fg: var(--ink-muted);
--font-size: calc(0.8125rem * var(--scale));
display: block;
}
.field-error, l-field-error {
--tone: var(--danger);
--fg: var(--tone-ink);
--font-size: calc(0.8125rem * var(--scale));
display: block;
}
Containers
.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, l-toolbar {
--gap: 0.5em;
--pad: 0.5em;
display: flex;
flex-wrap: wrap;
align-items: center;
}
Empty state
.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, l-empty-title {
--fg: var(--ink);
--weight: var(--weight-medium);
--font-size: calc(1rem * var(--scale));
display: block;
}
.empty-note, l-empty-note {
--font-size: calc(0.875rem * var(--scale));
display: block;
max-inline-size: 40ch;
}
Navigation
.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, l-steps {
--gap: 0;
display: flex;
counter-reset: largen-step;
list-style: none;
margin: 0;
--pad: 0;
}
Conversation
.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, 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, 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, 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 {
--gap: 1rem;
display: flex;
flex-direction: column;
}
.row {
--gap: 1rem;
display: flex;
align-items: center;
}
.cluster {
--gap: 0.5rem;
display: flex;
flex-wrap: wrap;
align-items: center;
}
.grid {
--gap: 1rem;
--min-item: 16rem;
display: grid;
grid-template-columns: repeat(auto-fill, minmax(min(var(--min-item), 100%), 1fr));
}
.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.