Undefined Css Variable Validation
Design Log #174 — Undefined CSS Variable Validation
Written for AI agents. See Log Methodology Note below for details.
Background
The design-system-validator plugin validates CSS values against DESIGN.md tokens. It uses css-cascade.ts to resolve styles per element, and token-matcher.ts to check values against tokens.
Currently, every matchColor, matchSpacing, matchRounded, etc. function in token-matcher.ts has a guard:
if (value.startsWith('var(')) return { matches: true };
And resolveVarReferences() in css-cascade.ts resolves var(--name) against :root-defined custom properties. If a variable isn't defined, the var() expression passes through unresolved — and the token matchers accept it unconditionally.
This means var(--color-that-doesnt-exist) passes validation silently.
CSS available to validators
The jay-html parser (jay-html-parser.ts:extractCss) already collects CSS from both sources into ctx.css:
<style>tags in<head><link rel="stylesheet">tags pointing to local files (resolved relative to the jay-html file, with@importresolution)
External URLs (http://, https://, //) are skipped. So the validator sees all locally-available CSS.
Problem
In jay-website/src/pages/aiditor-intro/page.jay-html, the CSS uses var(--color-surface-tint). This variable is referenced in DESIGN.md as a token name but never defined as a CSS custom property in any <style> block or linked stylesheet. The page renders with no color where one was intended.
The validator should catch undefined CSS variable references.
Design
New validator: design-undefined-vars
Add a 6th validator to the design-system-validator plugin that scans all CSS in a page for var(--name) references and reports any that are not defined as custom properties in the page's CSS.
What counts as "defined"
A CSS custom property --name is defined if it appears as a declaration (--name: value) in any rule within the page's CSS (both inline <style> and linked local stylesheets). This includes :root, html, scoped selectors (.dark { --color-bg: ... }), and media queries.
Rationale: if a variable is declared anywhere in the page's CSS, the author has defined it. Whether it's :root-scoped or conditionally scoped (.dark, @media) — the definition exists. The validator's job is to catch typos and missing definitions, not to do element-level scope analysis.
What counts as "used"
Any var(--name) or var(--name, fallback) occurrence in any CSS declaration value.
Severity
warning— same as other design-system findings.
Fallback handling
var(--name, fallback) has a fallback, so the missing definition is less severe but still worth flagging — the intent was clearly to use the variable, not the fallback. Report it as a warning with a note that a fallback exists.
Suppression
Use the same /* design-system: allow */ comment mechanism that works for other design-system warnings. When a var() reference is in a declaration followed by the allow comment, skip it.
Since this validator works at the CSS level (not per-element cascade), the implementation scans raw PostCSS declarations. If a declaration using var(--undefined) has the allow comment, that usage is excluded from the "used" set.
What NOT to validate
- Variables set via JavaScript — not statically determinable.
- Variables defined in external CDN stylesheets — not available to the parser.
Implementation approach
The validation is a standalone pass over the raw CSS text — it doesn't need the cascade or element resolution. It:
- Parses CSS with PostCSS
- Collects all
--*declarations from all rules →definedVars: Set<string> - Walks all declarations, extracts
var(--name)references (skipping/* design-system: allow */declarations) →usedVars: Map<string, {selector, property, hasFallback, fallbackValue}> - Reports
usedVarsentries not indefinedVars
Message format
Without fallback:
CSS variable "--color-surface-tint" is used but never defined
With fallback:
CSS variable "--color-surface-tint" is used but never defined (falls back to "red")
Suggestion (always):
Define --color-surface-tint in a :root block, or replace with a DESIGN.md token value directly.
To suppress: add /* design-system: allow */ after the declaration.
See agent-kit/designer/design-system.md for usage guide.
Integration
- New file:
validators/design-undefined-vars.ts - Export from
index.ts - Register in
plugin.yamlas 6th validator - Update
agent-kit/designer/design-system.mdwith the new validation error example - Test file:
test/validators/design-undefined-vars.test.ts
Implementation Plan
Phase 1: Validator implementation
- Create
lib/validators/design-undefined-vars.tswithvalidateUndefinedVars: JayHtmlValidatorFn - Use PostCSS to parse
ctx.css, collect all--*declarations as defined vars, collectvar(--name)references (excluding suppressed declarations) - Report findings for used-but-not-defined vars
Phase 2: Wire up
- Export from
lib/index.ts - Add to
plugin.yamlvalidators list
Phase 3: Tests
- Create
test/validators/design-undefined-vars.test.tsfollowing the pattern fromdesign-tokens.test.ts - Test cases:
- Flags
var(--x)when--xnot defined anywhere - Passes when
--xdefined in:root - Passes when
--xdefined inhtml - Passes when
--xdefined in a scoped selector (.dark { --x: ... }) - Passes when
--xdefined inside a@mediaquery - Notes fallback when present:
var(--x, red) - Multiple undefined vars
- No findings when no
var()used - No findings when no CSS
- Suppression with
/* design-system: allow */ - Var defined by one declaration, used by another — passes
- Var referencing another var:
--a: var(--b)— both--aand--bmust be defined
- Flags
Phase 4: Agent-kit guide update
- Add "Undefined CSS variable" section to
agent-kit/designer/design-system.mdunder Validation Errors
Verification Criteria
- Running
jay-stack validateon a project withvar(--color-surface-tint)(undefined) produces a warning - Running on a project with all vars defined produces no warnings from this validator
- Suppression via
/* design-system: allow */silences the warning - All existing tests continue to pass (this is additive — no changes to existing validators)
Implementation Refinements
Headfull component CSS handling (2026-08-30)
Problem: Headfull components (e.g., site-header) use CSS variables inherited from the page (var(--color-primary), etc.) but the undefined-vars validator ran per-file, flagging all inherited variables as undefined in standalone component files.
Attempting to fix by linking theme.css from the component's <head> caused two secondary issues:
- Duplicate detection — the same
@font-faceand@keyframesappeared twice (page + component both linking the same file), producing false "defined multiple times" errors - CSS parsing — the
@keyframesname regex (/@keyframes\s+(\S+)/g) misattributedfrominside keyframe bodies as a keyframe name when CSS was duplicated
Design decision: Components naturally inherit CSS variables from pages at runtime (CSS cascade). Requiring components to re-link the source file fights the cascade. Instead:
- The validator skips standalone component files (paths not under
pages/). Components are validated only in the context of the page that uses them. - When validating a page, the merged CSS includes component CSS. If a component uses
var(--x)and the page doesn't define it, the page-level validation catches it with a precise message:CSS variable "--x" used by component "site-header" is not defined — add it to the page's CSS or a linked stylesheet. - Component CSS gets a
/* Component: name */source comment during merge so the validator can trace which component introduced the var usage.
CSS deduplication fix: extractCss now accepts a skipPaths parameter. When parsing headfull component CSS, the page's already-resolved linked CSS file paths are passed through — the component skips reading files the page already links, preventing content duplication.
Changes:
jay-html-parser.ts— collect page CSS paths before headfull parsing, passskipPathstoextractCss, add/* Component: name */source commentsdesign-undefined-vars.ts— skip non-page files, extract component source from comments, improved message format- Tests added for component skipping and source attribution
Log Methodology Note
Note: These design logs are written primarily for AI agents as part of the Design Log methodology and made accessible here for human readers. The language and structure are optimized for machine consumption — expect precise, specification-style prose rather than narrative documentation.