Authoring a component
The nine rules, the ten ways it goes wrong when you break them, and the MCP server that checks what you wrote.
A complete 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;
}
}
<notification data-tone="warning">Two accounts need review.</notification> <div class="notification" data-tone="warning">…same thing…</div>
That is a complete component: seven tones, four variants, five sizes, every state and both themes, none of which it mentions.
The rules
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.
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.
--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.
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.
<dialog> is the modal, <details> is the collapse. Both are already themed by largen.elements. Reimplementing focus trapping or open-state management in CSS and JS buys nothing the platform has not already shipped and tested.
<notification> is fine; <button-primary> is not: it claims a role it lacks. A coined tag has no role and no accessible name, so assistive technology sees a span. largen paints from slots, so <button class="button-primary"> styles identically and keeps the role, focus ring and keyboard behaviour. Use the native element, or set role=.
With universal syntax, omitting initial-value makes an unset slot the guaranteed-invalid value, so var(--pad, revert-layer) falls back and hands the property back to the UA stylesheet untouched. Add an initial value and the slot is never unset, the fallback never fires, and the universal paint rule resets every element's UA defaults. Every <ul> loses its indent and every <h1> its size. This descriptor is the reason largen needs no build step.
One registered property does carry an initial value, deliberately: --scale is declared initial-value: 1. It is not a paint slot, since nothing reads it through var(--scale, revert-layer). It is a multiplier that every size calculation depends on, so it must always resolve to a number. largen verify exempts it by name for exactly this reason. The rule holds for the paint slots without exception.
Larger systems have three tiers: reference (a raw palette, blue-500), system (roles, --primary), component. largen has two — tokens, which are roles, and slots, which a component fills — and a theme writes values straight into the semantic tokens with no palette between. At this size that is one less indirection to hold in your head, not an omission.
A project outgrows it predictably: when one brand palette feeds several themes and the same hex appears in three files. Do not wait for largen to ship a reference tier — put the palette in your own theme, above largen's tokens, and point the roles at it: --brand-blue-60: #1c6fd6; --primary: var(--brand-blue-60). largen needs to know nothing about it, because a theme sets largen's token names and where it got them is its own business.
The placement that does not work is a palette in a component: naming --brand-blue-60 there pins it to one colour and drops it out of the tone axis, the same mistake as naming --danger directly.
The axes are tone, variant, size and state. Some systems add a context dimension — density, form factor, writing direction — and largen does not, now or planned. Said out loud so nobody waits for it: there will be no data-density.
The escape hatch is ordinary CSS and it is normal. A media or container query INSIDE a component is allowed by every rule here: @container (inline-size < 30em) { .toolbar { --pad: .5em .75em } }, beside the component it tightens. That queries the container rather than adding a variant, so it composes with tone, size and state instead of multiplying against them. What it must not become is a size variant: data-size already sets --scale, and a rule re-setting --pad keyed off a size-axis value is what largen verify reports.
Direction needs nothing. largen is written in logical properties throughout — padding-inline, margin-block, inset-inline-start — so a component built the same way follows dir="rtl" with no rule of its own. Reaching for padding-left is what breaks it.
When it goes wrong
The first one is the one to memorise. It is the only failure in largen that looks like success.
The declaration is dropped and the element paints transparent, with no warning anywhere. Nothing about --bg says "colour only". It is named for background, and every other slot takes whatever its property takes. This is the general shape for anything the algebra does not model: write the slot, then the plain property. The slot keeps the element inside the algebra; the plain declaration adds what has no slot.
--scale inherits and a component multiplies its --font-size by it. Padding in em is relative to that font-size and follows it: 0.5em 1em goes from 7px 14px at sm to 10px 20px at xl. In rem it is 8px 16px at every size. The page looks deliberate and has lost an axis. Reaching for the spacing scale is the obvious move, which is why largen verify reports this rather than leaving it to the eye.
This is the worst failure mode in the system because it looks like it works. An unlayered component outranks largen.modifiers, so the variant rules lose. Tone and size act through inheriting custom properties rather than by overriding slots, so they are unaffected. One dead axis and two live ones reads as "variant is buggy" rather than "my component is in the wrong layer".
Raw semantic tokens are absolute; the --tone* family is relative to whatever tone is in scope. Naming the absolute one opts the component out of the axis entirely, which is invisible until someone sets a tone on an ancestor.
revert-layer is only meaningful from inside a layer, and only reached when the slot is guaranteed-invalid. Break either condition and the one universal rule stops being inert and starts being destructive.
Writing @layer app.base, largen.components, app.overrides; reads as "app.base lowest, app.overrides highest" and does not do that. largen.components already exists and keeps its position, while the new app parent is appended after everything. So app.base outranks largen, and app.overrides can never reach past it, because both are children of one parent that has one position. Flat names avoid this because app-base and app-overrides are independent, so they can sit on either side.
This is also where a preflight goes when largen runs alongside another framework. Put its base layer in the statement ahead of largen, or it sorts last and flattens everything largen styled. Layer order beats specificity, so no amount of selector weight recovers it.
List ALL EIGHT largen sublayers, largen.fallback first. A sublayer the statement omits is created when largen loads and appended after the ones listed. For largen.fallback that puts its per-element resets above largen.components in exactly the engines the fallback exists for (Firefox 113–127, Safari 16.2–16.3), while looking correct in every engine you are likely to test in.
largen verify cannot catch this. Layer position is a property of the whole document at load time, of which files were seen and in what order, and a linter reading one stylesheet has no way to know. The browser is the only place the answer exists.
--tone-soft, --tone-ink and --tone-line are formulas, and largen recomputes them on every element, so setting --tone anywhere is enough for those. --tone-contrast is not a formula. It is the paired token (--danger pairs with --danger-on), and nothing can derive one from the other, so it keeps whatever value was already in scope. The three that follow --tone automatically are exactly the three that can be computed from it. largen verify and check_component_css both flag this.
A slot is registered with universal syntax and no initial-value, so initial returns it to the guaranteed-invalid value, the state it has when nothing has set it. var(--bg, revert-layer) then fires and hands the property back to the user-agent stylesheet untouched. Measured: a claimed element computes rgb(244,245,247) and 16px of padding; the same element with the slots set to initial computes rgba(0,0,0,0) and 0px.
This is the mechanism that makes incremental adoption possible. It is how largen can run beside another framework for as long as a migration takes, releasing whatever the other one still owns, rather than requiring a cutover. Treat it as a first-class part of the contract, not a trick.
The spelling that looks right is --fg: inherit, and it does something else. inherit takes the parent's computed --fg, and because the slot does not inherit and the parent never set it, that value is guaranteed-invalid. So var(--fg, revert-layer) fires and reverts to the user-agent stylesheet, which colours links blue. Measured against a parent at rgb(3,4,5): inherit gives rgb(0,0,238), currentColor gives rgb(3,4,5).
The general lesson is worth more than the recipe. Returning a slot to guaranteed-invalid gets you the user-agent value, never the ambient one. If you want what the surroundings have, name it with currentColor rather than trying to get there by removal.
Theme swapping works by changing what the tokens resolve to. Anything that bypasses the tokens is simply not part of that mechanism.
Check what you wrote
Locally, npx largen verify. Over MCP, check_component_css. Both run the same rules from the same module, so they cannot disagree.
Both are static. They have passed clean on visibly broken components before. Render the result in a browser, in both themes.
The MCP server
12 tools over Streamable HTTP, with no key, no account and no configuration. Everything served is public documentation or a pure function over what you send.
claude mcp add largen --transport http https://largen.dev/api/mcp
Orientation, before you write:
- get_contract: the slots, the axes, the layer rule and the failure modes
- list_components: every component a spec may name
- get_component_source: one reference component's CSS, for copying in
Checks, after you write:
- validate_spec: a model-emitted node tree against the allowlist
- check_component_css: the lint, same rules as largen verify
- render_spec: validate, render, and get a preview URL
Diagnostics, when something computes wrong:
- lookup_property: is this property a slot, and which
- resolve_cascade: which declaration wins on an element, and why, no browser
- explain_slot: does the paint rule apply this slot, or does it revert
- check_layer_order: where each @layer actually sorts across files
- emit_probe: a browser harness for what static analysis cannot see
- get_build: checksums of the served stylesheets, for drift checks
Your components, not ours
Every tool that reasons about components takes an optional components manifest, produced by npx largen manifest src/components.css. Supply it and the tools answer in your vocabulary; omit it and they fall back to the reference set. A malformed manifest is an error, never a silent fallback.
There is no generate_ui
This is a position, not a gap. You are already a capable model and you know the application being built; a model on this server would know none of that and add an API key, a cost and a latency budget to every call. The server equips you and then checks your work. You do the generating.
Without MCP
Fetch /llms-compact.txt: the whole contract inline, roughly 6,100 tokens, enough to author a correct component without another request.