Catalog and tokens
Two things decide what a Weft screen is allowed to say. The catalog is the vocabulary: which elements exist and what each may contain. Design tokens are the values of design: spacing, sizes, colors. Both are plain data files that you can read, extend and version.
The examples use the scratch folder from the tour; this block makes it again if you skipped that page.
$ mkdir -p weft-tour plugins/shared/scratch
$ cp corpus/login/screen.weft weft-tour/login.weft
The catalog
A catalog is a JSON file. It lists components; each component has a one-sentence description (written for a model), an ARIA role, what its content may be (nothing, text, elements, or both), and its props, slots, states and events. The core catalog, weft-core 0.1, has 29 components and lives in packages/catalog/catalog.json. SPEC §5 defines the file format and §5.1 lists every component.
| Group | Components |
|---|---|
| Structure | screen, stack, grid, section, tabs, tab, dialog |
| Text and media | heading, text, image, model, link, alert |
| Actions | button, menu, menu-item |
| Input | form, field, checkbox, switch, radio-group, radio, select, option, combobox, slider, stepper, date-picker, color-picker, segmented-control, segment |
| Collections | list, item, table, column, row, cell |
You can read the catalog from a terminal with the MCP tool (see MCP server):
$ node docs/examples/mcp-call.mjs weft_catalog kind=checkbox
{"kind":"checkbox","description":"A box the user ticks or clears to turn one independent option on or off.","role":"checkbox","content":"none","requiresLabel":true,"props":{"checked":{"description":"Whether the control is on; bind it to a boolean to read and write it.","type":"boolean","writable":true},"disabled":{"description":"Set to true to make the checkbox non-interactive.","type":"boolean"}},"events":["change"]}
Read the props carefully: type is a value type, values lists the allowed words of an enum, writable marks an input that writes back to your data, bindable: false means a literal only, references names the kind of element whose id the value must be (tabs.selected names a tab), and required means the validator will insist. Every component also takes the universal attributes of SPEC §2.2: id, label, hidden, state and on-<event>.
Extending it
There are two ways to go beyond the core catalog.
Vendor extensions. An element or attribute whose name starts with x-<vendor>- is allowed everywhere. An extension element must say which ARIA role it falls back to, so a renderer that does not know it can still show its children in a sensible container. This file has a chart element with a role, extra attributes and a slot, and it is valid even in strict mode:
$ weft validate compat/extensions.weft --catalog packages/catalog/catalog.json --strict; echo "exit $?"
exit 0
An unknown element that is not an extension is treated differently by readers and writers. A reader (lenient mode) warns and keeps it; a writer (strict mode) must not write it. See The weft command for the two modes and SPEC §8 for the rules.
Your own catalog. Copy catalog.json, change it and pass it with --catalog. This example adds a ghost value to the button's variant:
$ node -e 'const fs=require("fs");const c=JSON.parse(fs.readFileSync("packages/catalog/catalog.json","utf8"));c.name="acme";c.version="0.2.0";c.components.button.props.variant.values.push("ghost");fs.writeFileSync("weft-tour/acme.catalog.json",JSON.stringify(c,null,2))'
$ sed 's/variant="primary"/variant="ghost"/' weft-tour/login.weft > weft-tour/login-ghost.weft
$ weft validate weft-tour/login-ghost.weft --catalog packages/catalog/catalog.json --strict; echo "exit $?"
weft-tour/login-ghost.weft:8:61 W203 "ghost" is not an allowed value.
exit 1
$ weft validate weft-tour/login-ghost.weft --catalog weft-tour/acme.catalog.json --strict; echo "exit $?"
exit 0
A catalog does not have to be a full copy. Given with --catalog (or listed in a project's catalog), a catalog not named weft-core is merged over the core one, so it holds only what it adds: new components, and new values or props on existing ones.
A library. A catalog shared between projects names a prefix, and every component it adds starts with it: acme-ui (prefix acme) adds <acme-button> and <acme-card>. Libraries load side by side, before the project's own catalog, which widens them like the core. --catalog can be repeated, in that order:
$ weft validate examples/project/screens/review.weft --no-project --catalog examples/project/catalogs/acme-ui.catalog.json --catalog examples/project/catalog.json --strict; echo "exit $?"
exit 0
weft import-cem --prefix acme makes such a library from a Custom Elements Manifest. Projects shows one in use, and SPEC §10.4 has the rules.
An agent that is told which catalogs the host uses (weft_capabilities lists them, weft_catalog names each component's catalog) writes only what they allow. Libraries are shared as npm packages that a project lists as { "package": "@acme/ui" } (Publishing a catalog); Catalogs lists the known ones and their prefixes. The MCP server takes the project's catalogs (its --project, or a project argument); the design plugins use the core catalog.
Versioning a catalog
A catalog has its own version. Adding something is a minor change; removing or tightening something is a major change, because a screen that was valid could stop being valid. The core catalog is weft-core 0.2.0: it moved from 0.1.0 when its stacks and grids gained justify, padding, max-width and min-column-width, a minor change. diffCatalogs classifies a change by the rule in SPEC §8. From a file inside a workspace package (see the note in the index):
$ cat > plugins/shared/scratch/diff.ts <<'EOF'
import { readFileSync } from "node:fs";
import { coreCatalog, diffCatalogs } from "@weft/catalog";
const acme = JSON.parse(readFileSync("weft-tour/acme.catalog.json", "utf8"));
const { level, changes } = diffCatalogs(coreCatalog, acme);
console.log(level);
for (const c of changes) console.log(`${c.level} ${c.path}: ${c.message}`);
EOF
$ node plugins/shared/scratch/diff.ts
minor
minor components.button.props.variant.values: Enum value "ghost" was added.
Design tokens
Tokens are a file in the W3C Design Tokens format (DTCG 2025.10), the format design tools export. A screen never holds a color or a size; it holds a reference such as gap="{token.space.md}", so changing space.md in one place changes every screen. A designer can own that file.
packages/catalog/tokens/default.tokens.json is the default set: spacing (space.xs to space.xl), widths for max-width and min-column-width (size.sm 240 px to size.xl 960 px), radii, font sizes and a few colors, with aliases such as color.action.danger pointing at color.red. loadTokens reads a file, resolves aliases and returns the tokens and any problems it found, never throwing on bad input. Props that take a token say which type they accept (gap takes a dimension), and the validator checks both the name and the type.
The weft command and the MCP server do not know your tokens, so they accept any well-formed reference. To check names, give the validator the token map. It does the same for the list of action names your app has. This script checks the login screen four ways:
$ cat > plugins/shared/scratch/tokens.ts <<'EOF'
import { readFileSync } from "node:fs";
import { coreCatalog, loadTokens, tokenTypes } from "@weft/catalog";
import { parse } from "@weft/core";
const tokenFile = JSON.parse(readFileSync("packages/catalog/tokens/default.tokens.json", "utf8"));
const { tokens, problems } = loadTokens(tokenFile);
console.log(`${tokens.size} tokens, ${problems.length} problems`);
console.log(tokens.get("space.md"));
const login = readFileSync("weft-tour/login.weft", "utf8");
const check = (name: string, markup: string, actions?: string[]) => {
const { diagnostics } = parse(markup, {
catalog: coreCatalog,
mode: "strict",
tokens: tokenTypes(tokens),
...(actions === undefined ? {} : { actions }),
});
console.log(`${name}: ${diagnostics.length} diagnostics`);
for (const d of diagnostics) console.log(` ${[d.code, d.message, d.hint].filter(Boolean).join(" ")}`);
};
check("as written", login);
check("unknown token", login.replace("space.md", "space.huge"));
check("wrong token type", login.replace("space.md", "color.ink"));
check("unknown action", login, ["auth.submit", "nav.signup"]);
EOF
$ node plugins/shared/scratch/tokens.ts
19 tokens, 0 problems
{ type: 'dimension', value: { value: 16, unit: 'px' } }
as written: 0 diagnostics
unknown token: 1 diagnostics
W306 Token "space.huge" does not exist. did you mean "space.lg"?
wrong token type: 1 diagnostics
W307 Token "color.ink" has type color.
unknown action: 1 diagnostics
W308 Action "nav.reset" is not provided by the host.
W306 (unknown token), W307 (wrong type) and W308 (unknown action) only appear when you supply the lists. The renderer and the plugin scripts use the default tokens unless you pass --tokens, so they enforce W306 against that set.
Glass: a material token
A frosted-glass surface is a token too. DTCG has no material type, so a material is an ordinary color token (the tint; its alpha is the opacity) with one vendor extension, which every DTCG tool must keep, holding the background blur:
"material": {
"$type": "color",
"glass": {
"$value": { "colorSpace": "srgb", "components": [1, 1, 1], "alpha": 0.28, "hex": "#ffffff" },
"$extensions": { "dev.weft.material": { "blur": { "value": 20, "unit": "px" } } }
}
}
A stack or grid takes it with material="{token.material.glass}". The file is untrusted: an alpha outside 0 to 1, or a blur outside 0 to 100 px, is a problem and the token is left out. A mode (see the project's resolver) may give the dark theme its own tint and blur. The web targets blur what is behind the element with backdrop-filter and fall back to the solid colour; SwiftUI uses Liquid Glass on iOS 26 and later and a system material before it; Figma and Penpot get a fill and a background blur. See the corpus screen corpus/glass and SPEC §9 and §10.3.
Where tokens turn into values
| Where | What happens to {token.space.md} |
|---|---|
| Renderer | Replaced with the token's value from the set you gave, for example 16px. |
| React export | var(--weft-space-md): you define the CSS variable. |
| Import | Not recovered. A page holds only the resolved size. |
Limits
- The loader reads the DTCG subset Weft needs: groups,
$type,$value, aliases and$root. It does not read$extends,$refor the resolver module, and it ignores$description,$extensionsand$deprecated. - There is one set of tokens per run.
- A catalog extends the core and the libraries before it, and only widens them. A full copy of the core catalog, as in the example above, still works, but you keep it in step with the core one;
diffCatalogstells you what changed.