Skip to content

CSS linting and validation

FieldValue
TypeSkill Resource
Source~/.copilot/skills/design/references/css/linting.md
DescriptionNot specified

Source Content

CSS linting and validation

Two tools validate CSS: Stylelint for everything one stylesheet can check on its own, and four small Python scripts for the things that need contrast math or a view across many files. The rules they enforce are explained in ./hard-rules.md; this file is about running them.

Why Stylelint is primary

Stylelint is the standard CSS linter, so it earns its place by infrastructure, not novelty.

  • It is the ecosystem default. Most CSS developers already have Node, and Stylelint is the tool editors, formatters, and CI templates already expect.
  • It autofixes. --fix rewrites property order, quoting, and many violations in place, so you spend review time on judgment, not mechanics.
  • It plugs into editors and CI. The Stylelint VS Code extension underlines violations as you type, and a single CI step gates every pull request.

The old standalone Python CSS linter is retired and deleted — do not re-create a Python CSS linter. Stylelint owns everything a single stylesheet can validate; Python now covers only the gaps in the four checks.

Setup

A copyable, ready-to-run config lives at ../assets/stylelint/ — three files: stylelint.config.mjs (the ruleset), plugins/index.mjs (the custom css-skill rules), and package.json (the dependencies).

For ad-hoc linting without any project setup, use the wrapper ../../scripts/lint_css.sh. It resolves Stylelint from the repo’s node_modules, then the skill-level install in this folder, then PATH — and skips with a warning (exit 0) when none exists. Run it only when .css files changed.

Terminal window
# 1. Copy the template folder into your project root.
cp -R ~/.copilot/skills/design/references/assets/stylelint/. .
# 2. Install Stylelint and its plugins (one time).
npm install
# 3. Lint every stylesheet; add --fix to autofix what can be fixed.
npx stylelint "**/*.css"
npx stylelint "**/*.css" --fix

Wire it into the two places it pays off:

  • Editor. Install the Stylelint VS Code extension; it reads the same stylelint.config.mjs, so violations surface inline with no extra setup.
  • CI. Add one step — npx stylelint "**/*.css" — to the pipeline. A non-zero exit fails the build, so no violation merges.

What Stylelint enforces

Each row maps a Stylelint rule to the plain-English rule it guards. All rules are configured in ../assets/stylelint/stylelint.config.mjs; the custom ones are implemented in ../assets/stylelint/plugins/index.mjs. The config extends stylelint-config-standard and stylelint-config-recess-order (property ordering), and loads the stylelint-a11y plugin plus the local css-skill plugin.

Stylelint ruleGuards (see ./hard-rules.md)
declaration-no-importantNo !important outside the print/legal layer.
no-duplicate-selectorsNever duplicate — one home per selector (rule 5).
declaration-property-value-disallowed-list (color props)Tokens only — raw hex / rgb() / hsl() banned on color, background-color, border-color, outline-color, fill, stroke (rule 1).
declaration-property-value-disallowed-list (transition)Scope transitions — the keyword all is banned; name the properties (rule 12).
selector-max-specificity (0,3,0)Flat cascade — no specificity wars.
max-nesting-depth (3)Shallow nesting — keep selectors readable.
property-disallowed-list (physical box props, warning)Logical properties — prefer margin-inline / -block and padding-inline / -block for RTL safety (rule 3).
a11y/no-outline-noneVisible focus — never kill the focus outline (rule 8).
a11y/media-prefers-reduced-motionMotion respects prefers-reduced-motion.
css-skill/symmetric-paddingSymmetric padding — flags unequal opposing axes (rule 3).
css-skill/no-component-marginsMargins at the layout layer — flags margin / margin-* on components (rule 4).

The two custom css-skill rules carry deliberate escape hatches:

css-skill/symmetric-padding

Flags a padding shorthand whose top != bottom or left != right, and a rule where padding-top != padding-bottom or padding-left != padding-right. Suppress an intentional asymmetry by putting the word asymmetric in an adjacent comment — same convention the hard rule describes.

css-skill/no-component-margins

Flags any margin / margin-* declaration except value 0 and value auto, because spacing between components belongs to the layout parent. It is switched off by file glob in the layers where margins are legitimate (see per-file overrides).

Per-file overrides

The token, print, and layout layers have different rules, so the config relaxes specific checks by file glob. The intent matches ./hard-rules.md: raw values are legal where tokens are defined, physical props and !important are legal on paper, and margins are legal at the layout layer.

File globTurned offWhy
tokens*.css, theme*.css, variables*.css, *tokens.csscolor disallowed-list, css-skill/no-component-marginsThe token layer is the one place raw color literals are declared.
print*.css, legal*.cssdeclaration-no-important, property-disallowed-list, css-skill/no-component-marginsPaper needs physical box props and !important to force a print look. See ./print-and-legal.md.
layout*.css, global*.css, app.css, index.css, main.css, styles.csscss-skill/no-component-marginsMargins are allowed at the layout layer that owns spacing between components.

What Stylelint does not do

Stylelint reasons about one stylesheet at a time and has no color-perception model. It cannot:

  • Do the contrast math that decides whether a foreground/background token pair passes WCAG.
  • See across files or languages — that a component folder is missing its .css, that a theme block dropped a token the others define, or that the total stylesheet payload has grown too large.

Those gaps are exactly what the Python checks cover. They are Python 3 stdlib only — no pip install, so they run anywhere and in CI unchanged.

The four Python checks

Run each directly with python3. Exit 0 is clean; a non-zero exit means fix the reported ERRORs.

check_contrast.py

WCAG contrast across a theme’s token pairs. It parses the custom properties from a token file, auto-pairs each --X-foreground with its --X background per theme block, and reports the ratio against the WCAG thresholds. (Already exists — this is the one check Stylelint can never do.)

Terminal window
python3 ~/.copilot/skills/design/scripts/check_contrast.py <tokens.css>

Catches: any color pair below AA, in any theme block, before it ships.

check_colocation.py

Every UI component must have a co-located .css file (rule 10). It walks a directory, identifies likely visual components by heuristic, and flags any that lack a sibling stylesheet.

Terminal window
python3 ~/.copilot/skills/design/scripts/check_colocation.py <dir>

Catches: a component folder with no .css home, so one-off styles cannot leak into global sheets. The heuristic is a best guess — a pure-logic component flagged by mistake just needs its empty .css file (which the rule wants anyway), and a genuinely style-free helper can be excluded.

check_theme_completeness.py

Every token defined in :root must be defined in every theme block. A token present in light but missing in dark falls back silently and breaks the theme.

Terminal window
python3 ~/.copilot/skills/design/scripts/check_theme_completeness.py <tokens.css>

Catches: a token defined in one theme but absent from another — the cross-block gap Stylelint cannot see.

check_css_budget.py

Stylesheet size and @import chain budget. Performance is a constraint, not an afterthought (./architecture.md), so this caps total CSS payload and how deep @import chains run.

Terminal window
python3 ~/.copilot/skills/design/scripts/check_css_budget.py <dir>

Catches: a stylesheet that has grown past budget or an @import chain deep enough to stall the render path.

Run order: pre-commit and CI

Run the same sequence locally (pre-commit) and in CI. Autofix first, then the structural checks, and fix every ERROR before calling the work done.

Terminal window
# 1. Stylelint: autofix the mechanical violations, then verify none remain.
npx stylelint "**/*.css" --fix
npx stylelint "**/*.css"
# 2. The four Python checks (stdlib only — no install needed).
python3 ~/.copilot/skills/design/scripts/check_contrast.py path/to/tokens.css
python3 ~/.copilot/skills/design/scripts/check_colocation.py path/to/components
python3 ~/.copilot/skills/design/scripts/check_theme_completeness.py path/to/tokens.css
python3 ~/.copilot/skills/design/scripts/check_css_budget.py path/to/styles

A non-zero exit from any step fails the build. Fix every ERROR, re-run until each step exits 0, then report done — never ship with a failing check.