CSS linting and validation
| Field | Value |
|---|---|
| Type | Skill Resource |
| Source | ~/.copilot/skills/design/references/css/linting.md |
| Description | Not specified |
Source Content
CSS linting and validation
- Why Stylelint is primary
- Setup
- What Stylelint enforces
- Per-file overrides
- What Stylelint does not do
- The four Python checks
- Run order: pre-commit and CI
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.
--fixrewrites 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.
# 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" --fixWire 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 rule | Guards (see ./hard-rules.md) |
|---|---|
declaration-no-important | No !important outside the print/legal layer. |
no-duplicate-selectors | Never 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-none | Visible focus — never kill the focus outline (rule 8). |
a11y/media-prefers-reduced-motion | Motion respects prefers-reduced-motion. |
css-skill/symmetric-padding | Symmetric padding — flags unequal opposing axes (rule 3). |
css-skill/no-component-margins | Margins 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 glob | Turned off | Why |
|---|---|---|
tokens*.css, theme*.css, variables*.css, *tokens.css | color disallowed-list, css-skill/no-component-margins | The token layer is the one place raw color literals are declared. |
print*.css, legal*.css | declaration-no-important, property-disallowed-list, css-skill/no-component-margins | Paper 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.css | css-skill/no-component-margins | Margins 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.)
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.
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.
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.
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.
# 1. Stylelint: autofix the mechanical violations, then verify none remain.npx stylelint "**/*.css" --fixnpx stylelint "**/*.css"
# 2. The four Python checks (stdlib only — no install needed).python3 ~/.copilot/skills/design/scripts/check_contrast.py path/to/tokens.csspython3 ~/.copilot/skills/design/scripts/check_colocation.py path/to/componentspython3 ~/.copilot/skills/design/scripts/check_theme_completeness.py path/to/tokens.csspython3 ~/.copilot/skills/design/scripts/check_css_budget.py path/to/stylesA 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.