Modern CSS

Opinionated rules for modern CSS, so agents and people write it consistently.

These rules sit on top of the Modern Web Guidance skill, which must be installed. It explains what each feature does, when it applies, and how to fall back. It leaves choices open: which color space, whether to scope, when to nest, how to size type. These rules settle them; retrieve its guides for everything else. Where the two disagree, the rule wins.

The stylelint-config-modern package enforces every rule a linter can. This skill gets agents writing those from the start, and carries the rest.

Browser support policy: features within Baseline Newly Available are used natively, without @supports fallbacks, as progressive enhancement.

Rules

Architecture

Organizing styles (@layer)

@layer elements, components;

@layer elements {
  a {
    text-decoration-skip-ink: auto;
  }
}

Encapsulating styles (@scope)

@scope (.card) to (.content) {
  h2 {
    font-size: var(--large);
  }
}

Nesting rules and at-rules (&)

a {
  color: var(--blue);

  &:hover {
    text-decoration: underline;
  }

  code {
    font-variant-numeric: tabular-nums;
  }
}

Ranged queries (20em < width <= 40em)

.card {
  @container (width <= 20em) {
    grid-template-columns: 1fr;
  }

  @container (20em < width <= 40em) {
    grid-template-columns: 1fr 1fr;
  }

  @container (width > 40em) {
    grid-template-columns: repeat(3, 1fr);
  }
}

Layout

Flow-relative layout (*-inline-*, *-block-*, cqi/vi, start/end)

.card {
  max-inline-size: 60ch;
  padding-block-start: 2cqi;
  text-align: start;
}

Two-value display (display: block flex)

nav {
  display: block flex;
}

.card {
  display: block grid;
}

Fluid spacing (cqi)

.card {
  container: card / inline-size;
  gap: 2cqi;
  padding: clamp(1rem, 0.5rem + 2cqi, 2rem);
}

Auto-growing text areas (field-sizing: content)

textarea {
  field-sizing: content;
  inline-size: 100%;
  max-block-size: 12lh;
  min-block-size: 3lh;
}

Typography

Fluid type sizes (clamp())

:root {
  --medium: clamp(1.3125rem, 1.1821rem + 0.6522cqi, 1.6875rem);
  --large: clamp(1.75rem, 1.5761rem + 0.8696cqi, 2.25rem);
}

Widow and orphan words (text-wrap)

h1,
h2,
h3 {
  text-wrap: balance;
}

body {
  text-wrap: pretty;
}

Colors

Perceptual uniform lightness (oklch())

:root {
  --success: oklch(40% 0.15 150deg);
  --danger: oklch(40% 0.2 25deg);
}

Respecting color preferences (light-dark())

:root {
  --surface: light-dark(oklch(98% 0 0deg), oklch(16% 0 0deg));
  --text: light-dark(oklch(35% 0 0deg), oklch(75% 0 0deg));
}

Relative color functions (oklch(from /* .. */) & color-mix() )

:root {
  --primary: oklch(55% 0.25 350deg);
  --primary-hover: oklch(from var(--primary) l c calc(h - 10deg));
  --primary-tint: color-mix(in oklab, var(--primary), oklch(100% 0 0deg) 80%);
}

Motion

Respecting motion preferences (prefers-reduced-motion)

@media (prefers-reduced-motion: no-preference) {
  .hero {
    animation: bounce-in 0.5s ease;
  }
}

Applying these rules

When asked to author, refactor, or review CSS:

  1. Identify which rules apply to the task; where several do, apply them all.
  2. Read surrounding files to see which rules are already in use; match their patterns.
  3. Apply the rules consistently across the change set, not just at the touch points.
  4. Retrieve the Modern Web Guidance guides for the task and apply them within these rules, skipping their fallbacks under the browser support policy above.
  5. If the project runs Stylelint with stylelint-config-modern, run it on the changed files and fix what it reports before finishing.

Gotchas

Traps neither the rules above nor Modern Web Guidance surface:

Examples

This site follows the rules:

Fetch one only when the project has no CSS of its own to match.