CSS - Rules and Conventions
1. Philosophy
- Mobile-first — Base styles target mobile; larger screens get
progressive enhancements via
min-widthqueries, never the reverse. - Baseline-first — Prefer features labeled Baseline on MDN;
non-Baseline features require an
@supportsguard plus a fallback. - Performance by default — Animate only compositable properties
(
transform,opacity). Animating layout properties causes reflow and is prohibited. - Maintainability over convenience — Custom properties for tokens, BEM for naming, cascade layers for priority. Zero magic numbers.
- Do no harm to accessibility — Styling never reduces usability. Focus, contrast and reduced-motion policy live in the Accessibility skill: reference it, do not duplicate it.
2. Minimum Versions
| Technology | Version |
|---|---|
| CSS | CSS3+ (modern) |
Baseline-first policy: "Baseline YYYY" features are safe to use;
experimental ones require an @supports guard plus fallback.
3. Mobile-First
When: writing any component or page style.
Rule: author base styles for mobile, then enhance with (width >=)
range-syntax queries (Baseline 2023; replaces the 767.98px hack).
Why: mobile constraints produce leaner defaults; desktop overrides
are additive instead of destructive.
/* Base: mobile */
.container {
display: grid;
grid-template-columns: 1fr;
gap: 1rem;
}
@media (width >= 768px) {
.container {
grid-template-columns: repeat(2, 1fr);
}
} /* tablet */
@media (width >= 1024px) {
.container {
grid-template-columns: repeat(3, 1fr);
}
} /* desktop */
/* Band query: @media (768px <= width < 1024px) */
| Breakpoint | Min width | Usage |
|---|---|---|
| Base (mobile) | 0px | Smartphones |
| Tablet | 768px | Tablets |
| Desktop | 1024px | Desktops |
| Large screen | 1280px | Large desktops |
4. Layout
Flexbox (one dimension)
When: navigation bars, toolbars, centering, rows of items. Rule: Flexbox for single-axis distribution; switch to Grid when alignment needs both axes. Why: each system solves its own axis problem cleanly.
.navbar {
display: flex;
align-items: center;
justify-content: space-between;
gap: 1rem;
flex-wrap: wrap;
}
.centered {
display: flex;
align-items: center;
justify-content: center;
}
.card-row {
display: flex;
flex-wrap: wrap;
gap: 1rem;
}
.card-row > * {
flex: 1 1 300px;
} /* wraps without media queries */
Controls: flex-direction, flex-wrap, flex, align-items,
justify-content, gap.
Grid (two dimensions)
When: page layouts, card grids, alignment spanning rows and columns.
Rule: define tracks with repeat(auto-fill, minmax(...)) so items
respond without breakpoints; named areas only on full page shells.
Why: intrinsic tracks remove most layout media queries.
.grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(min(250px, 100%), 1fr));
gap: 1.5rem;
}
/* Sidebar + content (mobile-first) */
.layout {
display: grid;
grid-template-columns: 1fr;
gap: 2rem;
}
@media (width >= 768px) {
.layout {
grid-template-columns: 250px 1fr;
}
}
.card {
display: grid;
grid-row: span 3;
grid-template-rows: subgrid;
}
Controls: grid-template-columns/rows, grid-column: 1 / -1,
grid-row: span N, place-items, gap.
Anchor Positioning (Baseline 2026)
When: tooltips, menus, popovers positioned relative to a trigger.
Rule: use anchor-name + position-anchor + position-area;
wrap in @supports with a plain fallback position.
Why: replaces JS positioning libraries; the fallback keeps older
browsers functional.
.trigger {
anchor-name: --tip-anchor;
}
.tooltip {
position: fixed;
top: 0; /* fallback */
@supports (position-area: top) {
top: revert;
position-anchor: --tip-anchor;
position-area: top;
position-try-fallbacks: flip-block; /* flip on overflow */
margin-bottom: 8px;
}
}
5. Naming — BEM Only
When: naming any class.
Rule: block__element--modifier. Blocks are independent components,
elements are parts (__), modifiers are variations (--).
Why: flat, predictable specificity and greppable names eliminate
specificity wars without !important.
.card {
border: 1px solid var(--color-border);
padding: var(--space-4);
}
.card--featured {
border-color: var(--color-primary);
}
.card__title {
font-size: var(--text-lg);
font-weight: 600;
}
.card__title--large {
font-size: var(--text-xl);
}
.card__button--primary {
background: var(--color-primary);
}
Utility classes belong in the utilities layer only (section 8). A full
utility-first system is owned by Tailwind CSS.
6. Custom Properties
When: any repeated value — colors, spacing, radii, shadows, motion,
z-index.
Rule: declare design tokens on :root; consume them everywhere with
var(). Hardcoded values are prohibited.
Why: theming and dark mode become variable swaps instead of
find-and-replace campaigns.
:root {
--color-primary: #6366f1;
--color-danger: #ef4444;
--color-text: #111827;
--color-bg: #ffffff;
--color-border: #e5e7eb;
--font-sans: system-ui, sans-serif;
--space-2: 0.5rem;
--space-4: 1rem;
--space-6: 1.5rem;
--space-8: 2rem;
--radius-md: 0.5rem;
--shadow-md: 0 4px 6px -1px rgb(0 0 0 / 0.1);
--transition-fast: 150ms ease;
--z-modal: 200;
}
.button {
background: var(--color-primary);
padding: var(--space-2) var(--space-4);
transition: background-color var(--transition-fast);
}
/* Scale tokens to project needs; these categories are the minimum */
Theming with Data Attributes
Override tokens per theme; components never change:
[data-theme="dark"] {
--color-bg: #111827;
--color-text: #f9fafb;
}
@property — Typed Variables (Baseline 2024)
Register a type when a custom property needs an initial value, inheritance control or transitionability:
@property --hue {
syntax: "<angle>";
inherits: false;
initial-value: 0deg;
}
.color-wheel {
background: hsl(var(--hue) 80% 50%);
transition: --hue 0.3s;
}
7. Native Nesting
When: styling a component subtree (Baseline 2023).
Rule: nest with & up to three levels deep; deeper nesting means
the component should be split.
Why: keeps related styles together without preprocessor build steps.
.card {
border: 1px solid var(--color-border);
padding: var(--space-4);
& .card__title {
font-size: var(--text-lg);
}
&:hover {
box-shadow: var(--shadow-md);
}
@media (width >= 768px) {
padding: var(--space-6);
}
}
8. Cascade Layers
When: structuring any stylesheet.
Rule: declare the layer order first, then place code in
reset → base → components → utilities. Later layers win over
earlier ones regardless of specificity.
Why: gives priority an explicit home instead of leaking it into
selectors and !important.
@layer reset, base, components, utilities;
/* Surgical reset: no global margin/padding wipe */
@layer reset {
*,
*::before,
*::after {
box-sizing: border-box;
}
body {
margin: 0;
line-height: 1.5;
}
img,
picture,
video,
svg {
display: block;
max-width: 100%;
}
input,
button,
textarea,
select {
font: inherit;
}
}
@layer base {
body {
font-family: var(--font-sans);
color: var(--color-text);
background: var(--color-bg);
}
}
@layer components {
.card {
border: 1px solid var(--color-border);
}
}
/* Utilities: highest priority; !important acceptable here */
@layer utilities {
.mt-4 {
margin-top: var(--space-4);
}
.hidden {
display: none !important;
}
}
Priority order: utilities > components > base > reset.
Layered styles always lose to unlayered styles — keep third-party
overrides unlayered deliberately.
Do NOT use a universal * { margin: 0; padding: 0 }: it wipes useful
spacing and leaks into third-party widgets.
@import is allowed only to assign layers:
@import url("reset.css") layer(reset);
9. Container Queries
When: a component must adapt to its container, not the viewport.
Rule: set container-type: inline-size on the wrapper, query it
with @container. Prefer container queries over viewport queries for
reusable components.
Why: the same card renders correctly in a sidebar, modal or full
page with zero duplicated variants.
.card-wrapper {
container: card / inline-size;
}
@container card (width >= 400px) {
.card {
display: grid;
grid-template-columns: 200px 1fr;
}
}
/* Style query (Baseline newly available 2026) */
@container card style(--featured: true) {
.card {
border-color: var(--color-primary);
}
}
Size queries are Baseline 2023; style queries are newly Baseline — gate them behind a browser-support check until browserslist covers them.
10. Modern Selectors
:has() — Parent Selector (Baseline 2023)
.card:has(img) {
grid-template-rows: auto 1fr;
}
.form-group:has(:invalid) {
border-color: var(--color-danger);
}
tr:has(input:checked) {
background: var(--color-bg-secondary);
}
Specificity Control — :where() and :is()
Use :where() for zero-specificity defaults; :is() takes the
specificity of its most specific argument — know which you need:
:where(.card, .panel) {
margin: 0;
} /* (0,0,0): easy to override */
:is(.card, #header) {
color: red;
} /* (1,0,0): because of #header */
.button:not(.button--primary) {
background: var(--color-bg);
}
Keyboard Focus
Never delete the outline without a visible replacement. Full focus policy lives in the Accessibility skill:
.button:focus-visible {
outline: 2px solid var(--color-primary);
outline-offset: 2px;
}
.form-group:focus-within {
border-color: var(--color-primary);
}
@scope — Style Scoping (Baseline 2025)
Limit a style block to a DOM subtree; lowers specificity and prevents leaks:
@scope (.card) to (.nested) {
.title {
font-size: var(--text-lg);
} /* stays inside .card */
}
:popover-open (Baseline 2024)
Complements the HTML Popover API (HTML):
.menu[popover] {
display: none;
}
.menu:popover-open {
display: block;
}
.menu::backdrop {
background: rgb(0 0 0 / 0.4);
}
11. Modern Functions
Rule: clamp() for fluid bounds, min()/max() for caps and
floors, calc() for mixed-unit math — instead of breakpoint duplication
wherever a continuous value works.
font-size: clamp(1rem, 0.75rem + 0.5vw, 1.125rem);
width: min(100%, 1200px);
padding: max(1rem, 2vw);
grid-template-columns: repeat(auto-fill, minmax(min(250px, 100%), 1fr));
height: calc(100vh - var(--header-height));
12. @supports Feature Queries
Guard every non-Baseline feature. Key guards used by this skill:
| Feature | Guard condition |
|---|---|
| Grid | (display: grid) |
| Container queries | (container-type: inline-size) |
| Native nesting | (selector(&)) |
| Anchor positioning | (position-area: top) |
field-sizing |
(field-sizing: content) |
interpolate-size |
(interpolate-size: allow-keywords) |
Combine conditions with and / or / not, e.g.
@supports (display: grid) and (container-type: inline-size).
13. Typography
Fluid Scale
Define headings once with clamp(). text-wrap: balance prevents
orphaned words; pretty avoids single-word paragraph last lines.
body {
font-family: var(--font-sans);
line-height: 1.6;
}
h1 {
font-size: clamp(1.75rem, 1rem + 2vw, 2.5rem);
line-height: 1.2;
}
h2 {
font-size: clamp(1.5rem, 1rem + 1.5vw, 2rem);
}
h3 {
font-size: clamp(1.25rem, 1rem + 0.5vw, 1.5rem);
}
h1,
h2,
h3 {
text-wrap: balance;
}
p {
max-width: 70ch;
text-wrap: pretty;
}
Leading Trim
text-box-trim removes glyph-leading space for pixel-perfect headings
(Chrome/Edge 133+, Safari 18.2+; verify Firefox status on MDN):
@supports (text-box-trim: trim-both) {
h1,
h2,
h3 {
text-box-trim: trim-both;
text-box-edge: cap alphabetic;
}
}
@font-face — Variable Fonts
@font-face {
font-family: "Inter";
src: url("/fonts/inter.woff2") format("woff2");
font-weight: 400 700; /* variable range */
font-display: swap; /* no invisible text during load */
unicode-range: U+0000-00FF;
}
Declare only variation axes the foundry documents.
-webkit-font-smoothing is non-standard macOS-only polish — never rely
on it.
14. Animations and Transitions
CSS owns animation. JavaScript enters only when CSS cannot express the behavior (physics, canvas, gesture sync — see the JavaScript skill).
Rule: animate transform and opacity only; everything else
(width, top, margin) forces layout every frame.
Why: compositor-only animation runs off the main thread and
protects INP.
Transitions
Prefer transitions over keyframes whenever a state pair exists:
.button {
transition:
background-color 150ms ease,
transform 150ms ease;
}
.button:hover {
background-color: var(--color-primary-hover);
transform: translateY(-1px);
}
Keyframes
Reserve @keyframes for multi-step or looping animation. Shorthand:
<name> <duration> <timing> <delay> <iteration> <direction> <fill-mode>.
.fade-up {
animation: fadeUp 300ms ease-out both;
}
@keyframes fadeUp {
from {
opacity: 0;
transform: translateY(10px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
Enter/Exit (@starting-style + allow-discrete)
Animate elements toggling display: none (modals, popovers, menus).
@starting-style defines the entry state; allow-discrete lets
display/overlay transition so exit animation completes before
removal.
.menu {
transition:
opacity 150ms ease,
display 150ms allow-discrete,
overlay 150ms allow-discrete;
@starting-style {
opacity: 0;
translate: 0 -10px;
}
}
.menu[hidden] {
opacity: 0;
}
Intrinsic Size — interpolate-size (NOT Baseline)
Enables height: auto transitions. Experimental, Chromium-first —
guard it and accept the fixed-height fallback:
@supports (interpolate-size: allow-keywords) {
:root {
interpolate-size: allow-keywords;
}
}
.accordion-panel {
overflow: hidden;
height: 0;
transition: height 200ms ease;
}
.accordion-panel.open {
height: auto;
}
View Transitions
Same-document transitions smooth SPA state changes. Guard with
document.startViewTransition in JS before styling:
::view-transition-old(root) {
animation: 300ms ease fadeOut;
}
::view-transition-new(root) {
animation: 300ms ease fadeIn;
}
Reduced Motion (mandatory)
Ship this with every animation; full policy lives in the Accessibility skill:
@media (prefers-reduced-motion: reduce) {
*,
*::before,
*::after {
animation-duration: 0.01ms !important;
animation-iteration-count: 1 !important;
transition-duration: 0.01ms !important;
scroll-behavior: auto !important;
}
}
Scroll-driven timelines remain experimental — not for production.
15. Responsive Media
img,
video {
max-width: 100%;
height: auto;
display: block;
}
.media-cover {
width: 100%;
height: 100%;
aspect-ratio: 16 / 9;
object-fit: cover;
}
.container {
width: 100%;
max-width: 1200px;
margin-inline: auto;
padding-inline: var(--space-4);
}
aspect-ratio reserves space (protects CLS); object-fit: cover crops
without distortion, contain letterboxes instead. Markup-level
responsive images belong to the HTML skill.
16. Logical Properties
Rule: use logical properties for anything direction-dependent.
Why: RTL/LTR layouts adapt automatically instead of doubling
selectors under [dir="rtl"].
| Physical | Use instead |
|---|---|
margin-left/right |
margin-inline |
padding-top/bottom |
padding-block |
border-left |
border-inline-start |
width / height |
inline-size / block-size |
left/right insets |
inset-inline |
top/bottom insets |
inset-block |
text-align: left |
text-align: start |
.element {
margin-inline: var(--space-4);
padding-block: var(--space-2);
border-inline-start: 1px solid var(--color-border);
inline-size: 100%;
}
17. Dark Mode and System Preferences
Rule: declare color-scheme first — it enables light-dark() and
correct UA chrome (scrollbars, form controls):
:root {
color-scheme: light dark; /* required for light-dark() */
--bg: light-dark(#ffffff, #1a1a1a);
--text: light-dark(#000000, #ffffff);
}
Pair with the HTML meta <meta name="color-scheme" content="light dark">.
Force a theme by swapping tokens via data-theme (section 6).
System Preference Queries
@media (prefers-color-scheme: dark) {
:root {
color-scheme: dark;
}
}
@media (prefers-contrast: high) {
:root {
--color-text: #000000;
--color-border: #000000;
}
}
@media (prefers-reduced-transparency: reduce) {
.modal-backdrop {
background: rgb(0 0 0 / 0.8);
}
}
/* Experimental: near-zero support today */
@media (prefers-reduced-data: reduce) {
.hero {
background-image: none;
}
}
light-dark() returns the light value unless color-scheme is set —
declare it or the function silently does nothing useful.
18. Scroll
html {
scroll-behavior: smooth;
}
/* Carousel: snap horizontally, stop at each item */
.scroll-container {
scroll-snap-type: x mandatory;
overflow-x: auto;
display: flex;
gap: 1rem;
}
.scroll-item {
scroll-snap-align: start;
scroll-snap-stop: always;
flex: 0 0 100%;
}
.modal-content {
overscroll-behavior: contain;
} /* modal chaining */
.app-shell {
scrollbar-gutter: stable;
} /* no shift on overflow */
Never put overscroll-behavior: none on body — it breaks mobile
scroll chaining; use contain on inner containers. Do not restyle
scrollbars with ::-webkit-scrollbar; prefer scrollbar-width /
scrollbar-color.
19. Cascade and Specificity
Rule: style exclusively with classes. IDs, inline styles and
!important are banned except inside the utilities layer.
Why: class-level specificity keeps every conflict resolvable by
source order and layers.
/* ❌ */
#header {
}
/* ✅ */
.site-header {
}
/* Acceptable ONLY inside @layer utilities */
.hidden {
display: none !important;
}
Layer order beats specificity; unlayered beats layered. Fix conflicts by moving code to the right layer, not by raising specificity.
20. CSS Performance
HTML-layer rules only. Measurement, budgets and loading strategy live in the Performance skill.
/* Skip rendering offscreen sections */
.section-below-fold {
content-visibility: auto;
contain-intrinsic-size: 0 500px; /* estimate prevents CLS */
}
.widget {
contain: layout style paint;
} /* isolate subtree re-render */
will-change discipline: apply only on elements with a measured animation problem, remove after; every hint allocates memory.
21. Form Controls
Ownership split: HTML owns structure, JavaScript owns behavior and validation logic, CSS owns native styling only (HTML, JavaScript).
input[type="checkbox"],
input[type="radio"],
input[type="range"],
progress {
accent-color: var(--color-primary); /* native accent */
}
input:user-invalid {
border-color: var(--color-danger);
} /* Baseline 2023 */
/* Controls grow with content (newly Baseline 2026 — guard it) */
@supports (field-sizing: content) {
textarea {
field-sizing: content;
min-height: 2lh;
max-height: 10lh;
}
}
With field-sizing, never combine fixed width/height — constrain
with min-*/max-*. For custom-styled controls start from
appearance: none plus font: inherit (reset, section 8). Error
announcement (role="alert", aria-invalid) is owned by
Accessibility.
22. Methodology
Before using any property, function or pattern not documented in this skill:
- MCP Context7 (priority) — query library/spec docs.
- MDN Web Docs — existence, Baseline status, browser support.
- Can I Use — verify against the project's browserslist targets.
- Official spec — CSS WG drafts for edge cases.
Hard rule: if it is neither in this skill nor verifiable against two authoritative sources, DO NOT USE IT. Document it as an assumption or risk to the orchestrator.
23. Prohibitions
- No
!importantoutside@layer utilitiesor a documented exception - No IDs, inline styles or
style="..."attributes for styling - No nesting deeper than 3 levels — split the component
- No
pxtypography — userem/clamp() - No hardcoded colors or magic numbers — use custom properties
- No
floator absolute positioning for general layout - No animating
width,height,top,left,margin - No universal
* { margin: 0; padding: 0 }resets - No images without
max-width: 100% - No
@importin CSS exceptlayer()assignment - No vendor prefixes on Baseline properties (e.g.
-webkit-backdrop-filter) - No
will-changespraying; measured problems only - No global
overscroll-behavior: noneonbody - No
::-webkit-scrollbarcustom styling - No
font-variation-settingsaxes the font does not declare - No decorative infinite animations without a reduced-motion escape
- No re-implementing rules owned by sibling skills — reference them
24. References
Structure and forms markup: HTML Behavior and validation logic: JavaScript WCAG, focus, reduced motion: Accessibility Core Web Vitals and budgets: Performance CSP and XSS: Security Utility-first framework: Tailwind CSS
Visual effects (filters, clip-path, masks, 3D transforms, colors beyond
light-dark()) were removed — candidate future visual-effects skill.
Last updated: 2026-08