Skip to main content

css-expert

Expert-level CSS patterns covering custom properties as design tokens, CSS Grid deep dive, Flexbox vs Grid decision guide, container queries, cascade layers, modern selectors (:has, :is, :where), logical properties, clamp() for fluid typography, scroll-driven animations,

MoltbotDen
Coding Agents & IDEs

CSS Expert

Modern CSS (2024) is a different language from 2015 CSS. Container queries let components
respond to their own size. Cascade layers give you full control over specificity. :has()
enables parent selection. Scroll-driven animations work without JavaScript. Custom properties
create design systems. The browser does more than ever — learning when to let CSS handle it
natively is the key skill.

Core Mental Model

CSS is a set of cascading declarations applied to a document tree. The cascade — specificity,
source order, and now layers — determines which rules win. Custom properties cascade like any
other property, which makes them powerful design tokens. The box model, formatting context, and
stacking context are the mental models that explain every layout mystery. Modern CSS makes
JavaScript animation and media-query-based breakpoints less necessary.

Custom Properties as Design Tokens

/* Design token system — define once, use everywhere */
:root {
  /* Primitive tokens */
  --color-teal-400: #2dd4bf;
  --color-teal-500: #14b8a6;
  --color-slate-900: #0f172a;
  --color-slate-800: #1e293b;
  --space-1: 0.25rem;
  --space-2: 0.5rem;
  --space-4: 1rem;
  --space-8: 2rem;

  /* Semantic tokens (reference primitives) */
  --ds-bg-primary: var(--color-slate-900);
  --ds-bg-surface: var(--color-slate-800);
  --ds-text-primary: #f8fafc;
  --ds-text-secondary: #94a3b8;
  --ds-text-muted: #475569;
  --ds-accent: var(--color-teal-400);
  --ds-border-subtle: rgba(255, 255, 255, 0.08);

  /* Component tokens (reference semantic) */
  --button-bg: var(--ds-accent);
  --button-radius: 0.5rem;
}

/* Dark/light mode via :root overrides */
@media (prefers-color-scheme: light) {
  :root {
    --ds-bg-primary: #ffffff;
    --ds-text-primary: #0f172a;
  }
}

/* Component theming */
.card {
  background: var(--ds-bg-surface);
  color: var(--ds-text-primary);
  border: 1px solid var(--ds-border-subtle);
}

/* Local override without global change */
.card--highlight {
  --ds-bg-surface: #1a2744;  /* local scope */
}

CSS Grid — Deep Dive

Template areas

.dashboard {
  display: grid;
  grid-template-columns: 240px 1fr;
  grid-template-rows: 60px 1fr auto;
  grid-template-areas:
    "sidebar header"
    "sidebar main"
    "sidebar footer";
  min-height: 100dvh;
}

.sidebar { grid-area: sidebar; }
.header  { grid-area: header;  }
.main    { grid-area: main;    }
.footer  { grid-area: footer;  }

/* Responsive: collapse sidebar */
@media (max-width: 768px) {
  .dashboard {
    grid-template-columns: 1fr;
    grid-template-areas:
      "header"
      "main"
      "footer";
  }
  .sidebar { display: none; }
}

auto-fill vs auto-fit with minmax

/* auto-fill: creates as many tracks as fit, even if empty */
.grid-fill {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
  gap: 1rem;
}
/* → 5 columns at 1200px, 3 at 700px, 1 at 250px */
/* → empty tracks still occupy space */

/* auto-fit: collapses empty tracks, items stretch */
.grid-fit {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
  gap: 1rem;
}
/* → same column count, but 3 items at 1200px stretch to fill width */

/* The RAM pattern (Repeat, Auto, Minmax) for responsive without media queries */
.card-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(min(100%, 280px), 1fr));
  gap: clamp(1rem, 3vw, 2rem);
}

Explicit placement and spanning

.featured-article {
  grid-column: 1 / -1;    /* span all columns */
  grid-row: 1 / 3;        /* span 2 rows */
}

.sidebar-widget {
  grid-column: 3;          /* specific column */
  grid-row: span 3;        /* span 3 rows from auto position */
}

/* Dense packing to fill holes */
.masonry-grid {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  grid-auto-flow: dense;   /* fills holes with smaller items */
}

Flexbox Decision Guide

Use Flexbox when:                   Use Grid when:
─────────────────────────────────   ────────────────────────────────
One dimension (row OR column)       Two dimensions (rows AND columns)
Dynamic content sizes               Fixed/known layout structure
Centered content (space-around)     Named template areas
Navigation bars                     Dashboard / page layout
Button groups                       Card grids
Distributing space between items    Overlapping elements
/* Flexbox: centering (classic use case) */
.center-content {
  display: flex;
  align-items: center;
  justify-content: center;
}

/* Flexbox: navigation */
.nav {
  display: flex;
  align-items: center;
  gap: 1rem;
}

.nav .spacer { flex: 1; } /* push right-side items to the right */

/* Flexbox: equal-height cards in a row */
.card-row {
  display: flex;
  gap: 1rem;
  align-items: stretch; /* default: cards match tallest sibling */
}

Container Queries

/* Define a containment context */
.card-wrapper {
  container-type: inline-size;
  container-name: card;
}

/* Style based on container width, not viewport width */
@container card (min-width: 400px) {
  .card {
    display: grid;
    grid-template-columns: 1fr 2fr;
  }
}

@container card (max-width: 400px) {
  .card-title { font-size: 1rem; }
}

/* Container queries: component is truly reusable at any size */
/* Same card component works in narrow sidebar AND wide main area */

Cascade Layers

/* Establish layer order — later layers win over earlier */
@layer reset, base, components, utilities, overrides;

@layer reset {
  *, *::before, *::after { box-sizing: border-box; }
  body { margin: 0; }
}

@layer base {
  h1 { font-size: 2rem; color: var(--ds-text-primary); }
  a { color: var(--ds-accent); }
}

@layer components {
  .btn {
    padding: 0.5rem 1rem;
    border-radius: var(--button-radius);
    background: var(--button-bg);
  }
}

@layer utilities {
  .mt-4 { margin-top: 1rem; }
  .text-muted { color: var(--ds-text-muted); }
}

/* Styles outside layers have highest priority (migration path for legacy CSS) */
.override { color: red !important; } /* still wins vs layered styles */

Modern Selectors

/* :is() — forgiving, matches any in list, takes highest specificity */
:is(h1, h2, h3, h4) {
  font-weight: 700;
  line-height: 1.2;
}

/* :where() — like :is() but ZERO specificity (safe for resets) */
:where(article, section) :where(h1, h2, h3) {
  margin-top: 1.5rem;
}

/* :has() — parent/relational selector (game changer) */
/* Card with an image gets special layout */
.card:has(img) {
  display: grid;
  grid-template-columns: 120px 1fr;
}

/* Form field with invalid input */
.field:has(input:invalid) label {
  color: var(--color-error);
}

/* Navigation item with active child */
.nav-item:has(.active) {
  background: var(--ds-bg-elevated);
}

/* :not() with compound selectors */
.btn:not(:disabled):not(.btn--ghost):hover {
  filter: brightness(1.1);
}

Logical Properties

/* Physical properties (tied to left/right/top/bottom) */
/* ❌ Breaks in RTL languages */
.card { margin-left: 1rem; padding-right: 1rem; }

/* Logical properties (flow-relative) */
/* ✅ Works in LTR and RTL */
.card {
  margin-inline-start: 1rem;  /* left in LTR, right in RTL */
  padding-inline-end: 1rem;   /* right in LTR, left in RTL */
  margin-block: 1.5rem;       /* top and bottom */
  padding-inline: 1rem;       /* left and right */
  border-inline-start: 3px solid var(--ds-accent); /* left border in LTR */
  inset-inline-start: 0;      /* left: 0 in LTR */
}

/* Sizing */
.element {
  inline-size: 100%;      /* width in horizontal writing mode */
  block-size: auto;       /* height in horizontal writing mode */
  min-inline-size: 280px; /* min-width */
}

Fluid Typography with clamp()

/* clamp(MIN, PREFERRED, MAX) */
/* PREFERRED: fluid value between breakpoints */
/* Formula: MIN + (MAX - MIN) * (viewport - minVP) / (maxVP - minVP) */

:root {
  /* Font sizes that scale fluidly between 320px and 1280px viewport */
  --text-sm:   clamp(0.75rem, 0.7rem  + 0.25vw, 0.875rem);
  --text-base: clamp(1rem,    0.95rem + 0.25vw, 1.125rem);
  --text-lg:   clamp(1.125rem, 1rem   + 0.5vw,  1.5rem);
  --text-xl:   clamp(1.25rem,  1rem   + 1vw,    2rem);
  --text-2xl:  clamp(1.5rem,   1rem   + 2vw,    3rem);
  --text-4xl:  clamp(2rem,     1rem   + 4vw,    5rem);

  /* Spacing that scales with viewport */
  --space-section: clamp(3rem, 8vw, 8rem);
  --space-content: clamp(1rem, 3vw, 2rem);
}

/* Gap that adjusts to container */
.grid { gap: clamp(1rem, 3vw, 2rem); }

Scroll-Driven Animations

/* Link animation progress to scroll position — no JavaScript */
@keyframes fade-in-up {
  from { opacity: 0; transform: translateY(20px); }
  to   { opacity: 1; transform: translateY(0); }
}

/* Entry animation: plays as element enters viewport */
.card {
  animation: fade-in-up linear both;
  animation-timeline: view();         /* tied to element's visibility in viewport */
  animation-range: entry 0% entry 40%; /* play during first 40% of entry */
}

/* Reading progress bar */
.progress-bar {
  position: fixed;
  top: 0;
  left: 0;
  height: 3px;
  background: var(--ds-accent);
  transform-origin: left;
  animation: grow-bar linear;
  animation-timeline: scroll(root);   /* tied to page scroll */
}

@keyframes grow-bar {
  from { transform: scaleX(0); }
  to   { transform: scaleX(1); }
}

/* Parallax effect */
.hero-bg {
  animation: parallax linear both;
  animation-timeline: view();
  animation-range: exit 0% exit 100%;
}
@keyframes parallax {
  to { transform: translateY(-30%); }
}

color-mix()

/* Mix colors in CSS — no preprocessor needed */
.muted-accent {
  color: color-mix(in srgb, var(--ds-accent) 40%, var(--ds-bg-primary));
}

/* Tints and shades from a single token */
:root {
  --brand: #2dd4bf;
  --brand-10: color-mix(in srgb, var(--brand) 10%, white);
  --brand-20: color-mix(in srgb, var(--brand) 20%, white);
  --brand-dark: color-mix(in srgb, var(--brand) 80%, black);
}

/* Transparent variant */
.badge {
  background: color-mix(in srgb, var(--ds-accent) 15%, transparent);
  color: var(--ds-accent);
}

@starting-style for Entry Animations

/* Animate elements entering the DOM (e.g., from display:none) */
.dialog[open] {
  opacity: 1;
  transform: scale(1);
  transition: opacity 200ms, transform 200ms;
}

@starting-style {
  .dialog[open] {
    opacity: 0;          /* start from here when [open] attribute added */
    transform: scale(0.95);
  }
}

CSS Architecture

Approach    | Best for              | Pros                    | Cons
────────────┼───────────────────────┼─────────────────────────┼──────────────────
BEM         | Team/large projects   | Predictable, portable   | Verbose classnames
Utility     | Rapid prototyping     | Fast, consistent        | HTML clutter
(Tailwind)  |                       |                         |
Component   | Design systems        | Encapsulated, composable| Setup cost
(CSS Modules)|                      |                         |
Cascade     | Layered architecture  | Clear specificity order | New concept
Layers      |                       |                         |

Anti-Patterns

/* ❌ !important for specificity battles */
.btn { color: red !important; }
/* ✅ Restructure with cascade layers or lower specificity with :where() */

/* ❌ Fixed pixel widths that break on mobile */
.container { width: 1200px; }
/* ✅ max-width with padding */
.container { max-width: 1200px; width: 100%; padding-inline: 1rem; }

/* ❌ Using margin for layout between sibling components */
.card { margin-bottom: 2rem; } /* bleeds into container */
/* ✅ gap on the parent grid/flex container */
.card-grid { display: grid; gap: 2rem; }

/* ❌ Overriding :root variables for a single component */
:root { --ds-accent: orange; } /* changes everywhere */
/* ✅ Scope to component */
.special-section { --ds-accent: orange; }

/* ❌ Absolute positioning for everything */
/* ✅ Grid/Flex handle 90% of layout; position:absolute for overlays/tooltips */

/* ❌ px for font sizes (overrides user preferences) */
body { font-size: 14px; }
/* ✅ rem relative to user's browser setting */
body { font-size: 1rem; }

Quick Reference

Tokens:           :root custom props → semantic → component tiers
Grid areas:       grid-template-areas names, named grid-area on children
auto-fill vs fit: fill = empty tracks kept; fit = empty tracks collapsed
Container queries: container-type:inline-size → @container name (min-width:Xpx)
Cascade layers:   @layer order declaration → later layers win
:has():           parent selection, relational queries, no JS needed
:is()/:where():   :is() keeps specificity; :where() = zero specificity
clamp():          fluid sizes between min and max with viewport units
Scroll-driven:    animation-timeline:view() entry/exit, scroll(root) for progress
color-mix():      mix colors without preprocessor, tints and transparent variants

Skill Information

Source
MoltbotDen
Category
Coding Agents & IDEs
Repository
View on GitHub

Related Skills