# largen 0.6.3: the complete authoring contract
A property algebra for CSS. Plain CSS, with no build step, no preprocessor and no plugin.
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.
## Writing a component
@layer largen.components {
.notification, notification {
--bg: var(--tone-soft);
--fg: var(--tone-ink);
--border-width: 0 0 0 3px;
--border-color: var(--tone);
--border-style: solid;
--radius: var(--radius-md);
--pad: .75em 1em;
--gap: .75em;
display: grid;
grid-template-columns: auto 1fr;
align-items: center;
}
}
Two accounts need review.
…same thing…
That is a complete component: seven tones, four variants, five sizes, every
state and both themes, none of which it mentions.
## Slots
--bg --fg --border-color --border-width --border-style --radius --pad --gap
--font-size --weight --line-height --letter-spacing --shadow --transition
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: ""; inherits: true; initial-value: 1; }
Inheriting, registered: --scale
Inheriting, unregistered: --tone --tone-contrast --tone-ink --tone-line
--tone-soft
`--scale` is the only *registered* inheriting property: it is declared
`@property --scale { syntax: ""; 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 `
` keeps its indent and an `
` 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.
## Axes
tone: data-tone, inherits
values: primary secondary success info warning danger neutral
Tone inherits, and that is the point rather than a convenience: `` 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: data-variant, does not inherit
values: solid 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 ``, 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: data-size, inherits
values: xs 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: (DOM state), from 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.
## Rules
Project-architecture rules — "Two token tiers", "Density and direction are not
axes" — are in `get_contract` section "rules" and at
https://largen.dev/docs/contract.
1. Author inside `@layer largen.components`. Override from outside it.
Unlayered CSS beats every layer. That is what makes overriding largen work
without `!important`. It is also exactly what breaks an unlayered
*component*: it outranks `largen.modifiers`, so `data-variant` silently stops
applying while tone and size keep working. If a variant "isn't applying",
check this before anything else.
2. A component sets slots. It does not set colours.
Write `--bg: var(--tone-soft)`, never `--bg: #fee`, and never `--bg:
var(--danger)`. A colour literal cannot follow a theme swap, and reaching
past `--tone*` to a raw semantic token breaks tone inheritance. The component
pins itself to one colour and stops responding to the `data-tone` on its
container. `largen verify` enforces both.
3. Never write a dark-mode rule.
`--tone-soft` and `--tone-ink` resolve against `--canvas` and `--ink`, so a
theme swap carries every component with it. If a component seems to need a
dark-mode rule, the algebra has failed to cover something, and that failure
is the bug worth fixing. A dark rule would only hide it.
4. Never write a size variant.
Multiply by `var(--scale)` where you set a size, express the rest in `em`,
and padding follows type automatically. Five hand-written size variants per
component is the cost the size axis exists to remove.
The rule is about not writing the same component five times. It is not a rule
about units, and the distinction matters for the shape it appears to forbid:
a control that shrinks its box while holding its type at one size. `em` is
what couples padding to type; `rem` is what decouples them. Set `--pad` in
`rem` and the box stops following the size axis while the type keeps up with
it. Measured at `data-size="lg"`: padding in `em` grows from 16px to 18px,
padding in `rem` stays at 12.8px. That is a unit choice inside one component,
not a size variant.
5. Reach for HTML first.
`