# Modern CSS Patterns

Use these CSS patterns instead of legacy workarounds. Each entry shows the modern approach that replaces verbose, JavaScript-dependent, or hack-based solutions.

When writing CSS, prefer these modern properties and functions. They reduce code, remove JavaScript dependencies, and improve maintainability.

## Layout

```css
/* Centering with grid */
.parent {
  display: grid;
  place-items: center;
}

/* Centering with flexbox */
.parent {
  display: flex;
  justify-content: center;
  align-items: center;
}

/* Centering with auto-margins */
.parent { display: grid; }
.child { margin: auto; }

/* Sticky headers (no JS scroll listeners) */
.header {
  position: sticky;
  top: 0;
}

/* Aspect ratios (no padding hack) */
.video {
  aspect-ratio: 16 / 9;
}

/* Gap (no margin hacks) */
.row {
  display: flex;
  gap: 16px;
}

/* Grid areas (no line numbers) */
.layout {
  display: grid;
  grid-template-areas: "header header" "sidebar main" "footer footer";
}

/* Subgrid (align nested grids) */
.child {
  display: grid;
  grid-template-columns: subgrid;
}

/* display: contents (wrapper transparent to parent grid/flex) */
.wrapper {
  display: contents;
}

/* Inset shorthand */
.overlay {
  position: absolute;
  inset: 0;
}

/* Logical properties (direction-aware) */
.box {
  margin-inline-start: 1rem;
  padding-block: 1rem;
}

/* Container queries (no media queries for components) */
.wrapper {
  container-type: inline-size;
}
@container (width > 400px) {
  .card {
    grid-template-columns: auto 1fr;
  }
}

/* Name-only container queries (no dummy size condition) */
.sidebar { container-name: sidebar; }
@container sidebar {
  .card { grid-auto-flow: column; }
}

/* Scrollbar gutter (prevent layout shift) */
body {
  scrollbar-gutter: stable;
}

/* Zoom (layout-aware scaling, no margin hacks) */
.thumbnail {
  zoom: 0.5;
}
/* vs: transform: scale(0.5) + margin-bottom: -50% + margin-right: -50% */

/* Anchor Positioning (no JS calculations) */
.popover {
  position-anchor: --anchor-el;
  position: absolute;
  top: anchor(bottom);
  left: anchor(center);
}
/* vs: getBoundingClientRect() + resize listeners + window scroll math */

/* Gap decorations (rule lines between grid/flex items, no item borders) */
.grid {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: 24px;
  column-rule: 1px solid #ddd;
  row-rule: 1px solid #ddd;
}

/* Scrollbar-aware viewport units (100vw subtracts scrollbar when gutter reserved) */
html { scrollbar-gutter: stable; }
.full-bleed { width: 100vw; margin-inline: calc(50% - 50vw); }

/* shape() (responsive clip paths, no SVG) */
.hero {
  clip-path: shape(
    from 0% 0%,
    line to 100% 0%,
    line to 100% 80%,
    curve to 0% 80% via 50% 105%,
    close
  );
}
/* vs: clip-path: path("M 0 0 L 800 0...") — px values, not responsive */

/* border-shape (true non-rectangular geometry, shadows and borders follow) */
.tooltip {
  border-shape: shape(from 0 0, hline 100%, line to 50% calc(100% + 12px), line to 0 100%, close);
}
/* vs: clip-path masks the visual but the underlying box stays rectangular */
```

## Typography

```css
/* Fluid type (no media queries) */
h1 {
  font-size: clamp(1rem, 2.5vw, 2rem);
}

/* Balanced headlines */
h1,
h2 {
  text-wrap: balance;
  max-width: 40ch;
}

/* Pretty long-form paragraphs (avoid orphan last-line words) */
article p {
  text-wrap: pretty;
}

/* Line clamp (no JS truncation) */
.card-title {
  display: -webkit-box;
  -webkit-line-clamp: 3;
  line-clamp: 3;
  overflow: hidden;
}

/* Variable fonts (one file, all weights) */
@font-face {
  font-family: "MyVar";
  src: url("MyVar.woff2");
  font-weight: 100 900;
}

/* Font loading (no FOIT) */
@font-face {
  font-family: "MyFont";
  src: url("MyFont.woff2");
  font-display: swap;
}

/* Drop caps */
.drop-cap::first-letter {
  initial-letter: 3;
}

/* Experimental outline-driven type scale */
:heading {
  font-size: calc(1rem * pow(1.2, 5 - sibling-index()));
}

/* Centering text on cap height (no line-height tweaks) */
.btn {
  text-box-trim: trim-both;
  text-box-edge: cap alphabetic;
}

/* Trim full-width punctuation spacing in CJK and quotes (no negative margins) */
.prose { text-spacing-trim: trim-start; }
```

## Color

```css
/* CSS variables */
:root {
  --primary: #7c3aed;
  --spacing: 16px;
}
.btn {
  background: var(--primary);
}

/* Color mixing (no preprocessor) */
.card {
  background: color-mix(in oklch, #3b82f6 60%, #ec4899);
}

/* oklch (perceptually uniform) */
--brand: oklch(0.55 0.2 264);
--brand-light: oklch(0.75 0.2 264);

/* Relative color syntax (color variants) */
.btn {
  background: oklch(from var(--brand) calc(l + 0.2) c h);
}

/* contrast-color() (automatic readable text, no hardcoding) */
.badge {
  color: contrast-color(var(--bg));
}
/* vs: color: white; or color: black; hardcoded per background */

/* Dark mode (no extra CSS) */
:root {
  color-scheme: light dark;
}

/* light-dark() function */
:root {
  color-scheme: light dark;
  color: light-dark(#111, #eee);
}
```

## Selectors

```css
/* :has() (parent selector, no JS) */
.form-group:has(input:invalid) { border-color: red; }

/* :is() (grouping) */
.card :is(h1, h2, h3) { margin-bottom: 0.5em; }

/* :where() (zero specificity resets) */
:where(ul, ol) { margin: 0; padding-inline-start: 1.5rem; }

/* :nth-child(n of selector) (count only matching elements) */
.item:nth-child(2n of .featured) { background: var(--accent); }

/* :focus-visible (no mouse outlines) */
button:focus-visible { outline: 2px solid var(--focus-color); }

/* Larger touch targets without changing visual size */
.button { position: relative; }
@media (pointer: coarse) {
  .button::after { content: ''; position: absolute; inset: -8px; }
}

/* :user-invalid (form validation, no JS) */
input:user-invalid { border-color: red; }
input:user-valid { border-color: green; }

/* :placeholder-shown (floating labels, no JS state) */
input:not(:placeholder-shown) + label,
input:focus + label {
  translate: 0 -1.4rem;
  font-size: 0.75rem;
}
/* input needs placeholder=" " for the pseudo-class to track empty state */

/* CSS Custom Highlight API (text highlighting, no DOM mutation) */
/* CSS */
::highlight(search) { background: yellow; color: black; }
/* JS — no DOM changes, works across element boundaries */
const range = new Range();
range.setStart(node, startOffset);
range.setEnd(node, endOffset);
CSS.highlights.set('search', new Highlight(range));
/* vs: el.innerHTML = el.innerHTML.replace(/term/g, '<mark>$&</mark>') — breaks DOM */

/* CSS nesting (no Sass needed) */
.nav {
  display: flex;
  a {
    color: #888;
    &:hover { color: white; }
  }
}

/* Media pseudo-classes (video state without event listeners) */
video:paused + .controls .play-icon { display: block; }
video:playing + .controls .pause-icon { display: block; }
video:buffering + .controls .spinner { display: block; }
video:muted + .controls .muted-badge { display: block; }
.player:has(video:playing) { outline: 2px solid currentColor; }
/* Safari 15.4+ and Firefox 150+ only. Chromium pending via Interop 2026. */
```

## Animation

```css
/* Scroll-linked animations */
.reveal {
  animation: reveal linear both;
  animation-timeline: view();
  animation-range: entry 0% entry 100%;
}

/* Entry animations (no JS timing) */
.card {
  transition:
    opacity 0.3s,
    transform 0.3s;
  @starting-style {
    opacity: 0;
    transform: translateY(10px);
  }
}

/* Animate display: none */
.panel {
  transition:
    opacity 0.2s,
    overlay 0.2s allow-discrete;
  transition-behavior: allow-discrete;
}

/* Independent transforms */
.icon {
  translate: 10px 0;
  rotate: 45deg;
  scale: 1.2;
}
.icon:hover {
  rotate: 90deg;
}

/* Height auto animation (no JS) */
:root {
  interpolate-size: allow-keywords;
}
.accordion {
  height: 0;
  overflow: hidden;
  transition: height 0.3s ease;
}
.accordion.open {
  height: auto;
}

/* Typed custom properties (animatable) */
@property --hue {
  syntax: "<angle>";
  inherits: false;
  initial-value: 0deg;
}

/* random() (per-element random values, no JS) */
.card {
  rotate: random(-15deg, 15deg);
  animation-delay: random(0s, 1s);
}
/* vs: forEach + Math.random() + el.style.setProperty() */

/* Motion path (no GSAP, no JS) */
@keyframes along-path {
  from {
    offset-distance: 0%;
  }
  to {
    offset-distance: 100%;
  }
}
.ball {
  offset-path: path("M 0 0 C 150 -100 300 100 450 0");
  offset-rotate: auto;
  animation: along-path 2s linear infinite;
}

/* Animate <details> open/close (no JS height measurement) */
:root { interpolate-size: allow-keywords; }
details::details-content {
  height: 0;
  overflow: clip;
  transition: height .3s ease, content-visibility .3s ease allow-discrete;
}
details[open]::details-content { height: auto; }

/* Auto-hiding header on scroll direction (no JS scroll listener + lastY) */
html { container-type: scroll-state; }
header { position: fixed; inset-block-start: 0; transition: translate .3s; }
@container scroll-state(scrolled: block-end) {
  header { translate: 0 -100%; }
}

/* Scroll-triggered animations (no IntersectionObserver + animate()) */
.reveal {
  animation: fade-up .6s ease-out both;
  animation-trigger: --t play-forwards;
  timeline-trigger-name: --t;
  timeline-trigger-source: view();
}

/* Two-axis position: sticky via single-axis scroller (no JS scroll sync) */
.table-wrap { overflow-x: auto; overflow-y: clip; }
th:first-child, td:first-child { position: sticky; left: 0; }
thead th { position: sticky; top: 0; }

/* Element-scoped view transitions (no FLIP recipe, no global snapshot) */
grid.startViewTransition(() => reorderItems());
.grid li { view-transition-name: match-element; }
```

## Components

```css
/* Scroll snap (no carousel library) */
.carousel {
  scroll-snap-type: x mandatory;
  overflow-x: auto;
  display: flex;
  gap: 1rem;
}
.carousel > * {
  scroll-snap-align: start;
}

/* Popover (no JS toggles) */
#menu[popover] {
  position: absolute;
  margin: 0.25rem 0;
}

/* Accent color (style form controls) */
input[type="checkbox"] {
  accent-color: #7c3aed;
}

/* Frosted glass (no opacity hacks) */
.glass {
  backdrop-filter: blur(12px) saturate(1.5);
  background: rgba(255, 255, 255, 0.1);
}

/* Object-fit (no background-image hack) */
img {
  object-fit: cover;
  width: 100%;
  height: 200px;
}

/* Overscroll behavior (no scroll chaining) */
.modal-content {
  overflow-y: auto;
  overscroll-behavior: contain;
}

/* Auto-growing textarea (no JS) */
textarea {
  field-sizing: content;
  min-height: 3lh;
}
```

## Architecture

```css
/* Cascade layers (organize CSS without specificity wars) */
@layer reset, base, components, utilities;
@layer components {
  .card { padding: 1rem; background: white; }
}
@layer utilities {
  .mt-4 { margin-top: 1rem; }  /* beats components automatically, no !important */
}

/* Scoped styles (no BEM) */
@scope (.card) {
  .title {
    font-size: 1.25rem;
  }
  .body {
    color: #444;
  }
}

/* Isolation (contain z-index within components) */
.card-container {
  isolation: isolate;
}
/* vs: managing z-index globally, or restructuring DOM */

/* Lazy rendering (no IntersectionObserver) */
.section {
  content-visibility: auto;
  contain-intrinsic-size: auto 500px;
}

/* Windows High Contrast support */
@media (forced-colors: active) {
  .btn {
    background: ButtonFace;
    color: ButtonText;
    border: 1px solid ButtonText;
    forced-color-adjust: none;
  }
}
```

## Assets & Performance

1.  **Minification**: Most primary CSS and JS assets are minified. Modify `modern.css` or `still-using-js.js` and run `python ../internal-tools/minify_assets.py` to update the `.min` versions.
2.  **Caching**: Data files are cached as PHP arrays in the `cache/` directory. Loaders in `loaders/` handle auto-regeneration.
3.  **Versioning**: Use `filemtime()` in PHP templates to append a version string (`?v=...`) to asset URLs for automatic cache-busting.

## Rules

1. Prefer CSS over JavaScript for layout, animation, and interaction when browser support allows.
2. Use CSS custom properties for theming. Use `@property` when you need typed or animatable values.
3. Use CSS nesting. The `&` prefix is optional for element selectors, required for pseudo-classes (`&:hover`) and compound selectors (`&.active`).
4. Use `oklch` for perceptually uniform color manipulation. Use `color-mix()` for blending.
5. Use `clamp()` for fluid values. Avoid media query breakpoints for font sizes.
6. Use logical properties (`inline`, `block`) over physical (`left`, `right`) for internationalization.
7. Use `gap` instead of margin hacks for spacing in flex and grid layouts.
8. Use `:has()` for parent selection instead of JavaScript class toggling.
9. Use `@layer` to manage specificity instead of `!important` or high-specificity selectors.
10. Check browser support at https://modern-css.com before using limited-availability features in production.
