CSS Hard Rules
| Field | Value |
|---|---|
| Type | Skill Resource |
| Source | ~/.copilot/skills/design/references/css/hard-rules.md |
| Description | Not specified |
Source Content
CSS Hard Rules
These are non-negotiable. SKILL.md gives a one-line reminder of each and links here; this file is the canonical home. Every rule states the rule, why it exists, how to apply it, and how it is enforced. How each rule is checked — Stylelint, a Python script, or judgment — is laid out in ./linting.md.
- 1. Tokens only
- 2. Theme-compatible always
- 3. Symmetric padding
- 4. Margins live at the layout layer
- 5. Never duplicate
- 6. BEM owns structure, Tailwind owns layout
- 7. Semantic HTML and semantic names
- 8. Visible focus
- 9. Print is black and white
- 10. Co-located component CSS
- 11. Great comments
- 12. Scope transitions
The rules
Enforced rules
1. Tokens only
Rule: never write a raw color, spacing, or radius literal in component CSS — reference var(--token). Raw values are allowed only in the token-definition layer where the tokens are declared.
Why: a literal hardcodes one theme and one scale into a file that should bend to all of them.
How to apply:
/* button.css — consumes tokens, declares none */.button { color: var(--color-fg); /* not #1a1a1a */ background: var(--color-bg); /* not white */ padding: var(--space-2) var(--space-4); /* not 8px 16px */ border-radius: var(--radius-md); /* not 6px */}Enforcement: Stylelint’s declaration-property-value-disallowed-list rejects raw hex and rgb()/rgba()/hsl()/hsla() literals on the color properties (color, background-color, border-color, outline-color, fill, stroke) and points you at var(--token); the token-layer override turns this off where literals are declared.
2. Theme-compatible always
Rule: assume multiple themes exist; colors come from tokens that flip via :root and [data-theme]. Never hardcode a color, and never conditionally swap classes per theme in markup.
Why: when the token flips, every component restyles for free — class-swapping puts theme logic in a hundred call sites.
How to apply:
/* tokens.css — the ONE place literals live */:root { --color-bg: #ffffff; --color-fg: #1a1a1a; }[data-theme="dark"] { --color-bg: #121212; --color-fg: #f5f5f5; }{/* Right: same class in every theme — the token flips */}<div className="card" />{/* Wrong: theme branching in markup */}<div className={isDark ? "card card--dark" : "card"} />Enforcement: hardcoded colors are caught by Rule 1; check_theme_completeness.py verifies every token is defined in every theme, and check_contrast.py checks each foreground/background pair for legibility. Theme-conditional class logic in markup is judgment — not statically checkable. Full model: ../tokens-and-theming.md.
3. Symmetric padding
Rule: padding-inline left == right and padding-block top == bottom, unless there is a specific, commented reason. Prefer the two-value shorthand or padding-inline / padding-block.
Why: symmetric padding is the default that reads as intentional; asymmetry without a note reads as a bug.
How to apply:
.card { /* symmetric: one value per axis */ padding-block: var(--space-4); padding-inline: var(--space-6);}
.alert { /* asymmetric on purpose: extra room for the close button at the end */ padding-block: var(--space-3); padding-inline: var(--space-4) var(--space-10);}Enforcement: the custom Stylelint rule css-skill/symmetric-padding flags a padding shorthand whose top != bottom or left != right, and unequal padding-top/padding-bottom or padding-left/padding-right within a rule; an adjacent comment containing the word asymmetric clears it. More: ../spacing.md.
4. Margins live at the layout layer, not on components
Rule: components own padding and internal spacing only. Outer spacing between components is the parent layout’s job, via gap or a stack/cluster primitive.
Why: a component cannot know the spacing of every layout it lands in; component margins become the hook people use to hack one-off spacing.
How to apply:
/* Wrong: the card decides its own outer gap */.card { margin-bottom: var(--space-4); }
/* Right: the layout owns the gap between children */.stack { display: flex; flex-direction: column; gap: var(--space-4); }Enforcement: the custom Stylelint rule css-skill/no-component-margins flags any margin / margin-* declaration except value 0 and value auto; layout, global, token, and print file overrides switch it off where margins belong. Layout primitives: ../spacing.md.
5. Never duplicate
Rule: do not repeat a declaration block; share it via a generic reusable class used in every place that needs it.
Why: duplicated CSS drifts — one copy gets fixed, the others rot.
How to apply:
/* Right: one reusable surface class, reused everywhere */.surface { background: var(--color-surface); border-radius: var(--radius-md); }/* .card and .panel both compose .surface instead of re-declaring it */Enforcement: Stylelint’s no-duplicate-selectors rejects a selector that appears more than once. Sharing patterns: ./architecture.md.
6. BEM owns structure, Tailwind owns layout/spacing
Rule: BEM classes own component structure and variants; Tailwind utilities own layout, spacing, and one-off visual tweaks. Never use both for the same responsibility.
Why: one owner per responsibility means there is exactly one place to change a thing — and no specificity war.
How to apply:
{/* Right: variant owned by BEM, layout owned by Tailwind */}<button className="button button--primary mt-0 self-start">Save</button>{/* Wrong: variant owned twice — which one wins? */}<button className="button button--primary bg-blue-600">Save</button>Enforcement: judgment — not statically checkable. The decision table and examples: ./bem-and-tailwind.md.
7. Semantic HTML and semantic class names
Rule: use the correct HTML element for the job, and name classes by role, never by appearance.
Why: role-based names survive a redesign; .red-box lies the moment the box turns gray.
How to apply:
<!-- Right: real element, role-named class --><button class="button button--danger">Delete</button><!-- Wrong: div acting as a button, appearance-named class --><div class="red-rounded-box" onclick="...">Delete</div>Enforcement: judgment — neither appearance-named classes nor non-semantic HTML are statically checkable. Naming standard: ./comments-and-naming.md.
8. Visible focus on every interactive element
Rule: every interactive element shows a visible focus style. Never outline: none / outline: 0 without an equally visible replacement.
Why: keyboard users navigate by the focus ring — removing it strands them, and it fails WCAG.
How to apply:
/* Right: replace the default outline, never just remove it */.button:focus-visible { outline: 2px solid var(--color-focus-ring); outline-offset: 2px;}Enforcement: the Stylelint a11y plugin rule a11y/no-outline-none rejects outline: none / outline: 0 that kills the focus outline.
9. Print is black and white
Rule: print styles must work in pure black and white and never assume color. There is one central print stylesheet — never a per-component @media print block.
Why: color print costs ink and is unreliable; one central sheet keeps print behavior in a single auditable place.
How to apply:
/* Wrong: color-dependent print rule scattered in a component file */@media print { .badge { background: var(--color-warning); } }
/* Right: lives in the central print stylesheet, B&W only */@media print { .badge { border: 1pt solid #000; background: none; } }Enforcement: judgment — central-stylesheet placement and black-and-white safety are not statically checkable. Discipline, the vendored reference, and the print file overrides: ./print-and-legal.md and ../assets/print.css.
10. Co-located component CSS
Rule: a component’s styles live in a .css file next to the component, and every component folder contains a .css file even if it is currently empty.
Why: an empty file is an obvious, findable home — it stops one-off styles from leaking into global sheets.
How to apply:
components/ button/ button.tsx button.css <- always present, even if emptyEnforcement: check_colocation.py flags a component folder with no co-located .css file. Layout and global structure: ./architecture.md.
11. Great comments
Rule: every CSS file opens with a header comment block, and non-obvious declarations carry a “why, not what” note.
Why: CSS is full of intentional-looking accidents; a one-line “why” is the difference between a fix and a regression.
How to apply:
/* =================================================================== * card.css — surface container for grouped content * Owns: padding, radius, surface color. Does NOT own outer margin. * =================================================================== */.card { /* clip the decorative top accent to the rounded corner */ overflow: hidden;}Enforcement: judgment — comment presence and quality are not statically checkable. Comment standard: ./comments-and-naming.md.
12. Scope transitions
Rule: never transition: all. Name the exact properties that animate.
Why: all transitions properties you never meant to (including layout), causing jank and surprise repaints.
How to apply:
/* Wrong: animates everything, including layout */.button { transition: all 150ms; }
/* Right: only what should move */.button { transition: background-color 150ms, color 150ms; }Enforcement: Stylelint’s declaration-property-value-disallowed-list rejects the keyword all on transition and transition-property and tells you to name the properties. Timing, easing, and reduced-motion: ../motion.md.