CSS#

CSS (Cascading Style Sheets) describes how HTML looks. Modern CSS (“CSS3” in marketing; in practice CSS3 is a long sequence of independently versioned modules) covers layout, animation, theming, and complex selection.

Selectors#

How CSS picks the elements to style. Modern selectors go far beyond element / class / id, with attribute matchers, combinators, pseudo-classes, and the newer :is() / :has() / :where() give the same expressiveness as a small query language.

/* element */
p { ... }

/* class / id */
.button { ... }
#main   { ... }

/* attribute */
input[type="email"] { ... }
a[href^="https://"] { ... }   /* starts with */
a[href$=".pdf"]     { ... }   /* ends with */

/* combinators */
nav a       { ... }    /* descendant */
nav > a     { ... }    /* direct child */
h2 + p      { ... }    /* adjacent sibling */
h2 ~ p      { ... }    /* subsequent siblings */

/* pseudo-classes */
a:hover, a:focus-visible { ... }
:nth-child(2n)   { ... }
input:invalid    { ... }
:is(h1, h2, h3) > a { ... }
:where(...)      /* same as :is but zero specificity */
:has(img) { ... }   /* parent based on its descendants */

/* pseudo-elements */
p::first-line { ... }
p::before { content: "» "; }

Specificity and the Cascade#

When multiple rules match an element, the more specific one wins; on a tie, the later one wins. The specificity story is one of CSS’s biggest sources of “why isn’t this working” debugging time; modern features (:where(), cascade layers, native nesting) reduce the surface area.

  • Inline style="...", highest in the source order.

  • IDs > classes / attributes / pseudo-classes > elements / pseudo-elements.

  • !important, escape hatch; avoid.

Modern alternatives that reduce specificity wars.

  • :where(), always zero specificity.

  • Cascade layers (@layer), explicit cascade ordering.

  • Native nesting, structural without raising specificity.

The Box Model#

Every element is a rectangle with content, padding, border, and margin. The default box-sizing: content-box makes width exclude padding and border, which surprises everyone the first time. Almost every modern stylesheet sets border-box globally:

*, *::before, *::after { box-sizing: border-box; }

border-box makes width include padding and border, almost always what you want.

Layout: Flexbox#

For one-dimensional layout, a row or a column. Flexbox handles alignment, distribution, and wrapping along a single axis; reach for it whenever the layout is “items in a line” rather than “rows and columns”:

.row {
  display: flex;
  gap: 1rem;
  align-items: center;
  justify-content: space-between;
}
.grow { flex: 1; }

Layout: Grid#

For two-dimensional layout. Grid handles “rows and columns” and is the right tool for page-scale structure (header, nav, main, footer), as well as for auto-fill / minmax patterns that build responsive card grids without media queries.

.page {
  display: grid;
  grid-template-columns: 240px 1fr;
  grid-template-rows: auto 1fr auto;
  grid-template-areas:
    "header header"
    "nav    main"
    "footer footer";
  min-height: 100vh;
}
header { grid-area: header; }
nav    { grid-area: nav; }
main   { grid-area: main; }
footer { grid-area: footer; }

Auto layouts.

.cards {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(240px, 1fr));
  gap: 1rem;
}

Custom Properties (CSS Variables)#

Native CSS variables that cascade, are dynamic at runtime, and can be set per component or per state. The most flexible piece of modern CSS; design tokens, theme switches, and component-local overrides all collapse into one mechanism.

:root {
  --bg: white;
  --fg: #111;
  --accent: #0b3d91;
}

@media (prefers-color-scheme: dark) {
  :root { --bg: #111; --fg: #eee; }
}

body { background: var(--bg); color: var(--fg); }
a    { color: var(--accent); }

Custom properties cascade, are dynamic at runtime, and can be set per component or even per state.

Modern Color#

Color in CSS has caught up with what designers actually want. OKLCH is perceptually uniform (a 10% lightness change looks the same regardless of hue); color-mix() derives hovers and tints without a preprocessor; wide-gamut color spaces unlock display capabilities that rgb() couldn’t reach.

/* OKLCH - perceptually uniform, wide gamut */
color: oklch(60% 0.15 250);
color: oklch(60% 0.15 250 / 0.5);   /* alpha */

/* color-mix to derive hovers / shades */
background: color-mix(in oklch, var(--accent) 80%, white);

Typography#

The typography defaults a healthy modern stylesheet sets. system-ui for native-looking fonts, sane line-heights, clamp() for fluid type that scales with viewport, and font-display: swap to keep web fonts from causing invisible-text regressions.

body {
  font-family: system-ui, sans-serif;
  line-height: 1.5;
  font-size: clamp(1rem, 0.95rem + 0.4vw, 1.125rem);
}
h1 { font-size: clamp(1.5rem, 1.2rem + 1vw, 2rem); line-height: 1.2; }
  • rem for type scales; em for component-relative spacing.

  • clamp() for fluid type and spacing.

  • font-display: swap; on web fonts to avoid invisible text.

Responsive Design#

Layouts that adapt to screen and container size. Mobile-first media queries are the long-standing default; container queries are the 2023+ addition that lets a component respond to its own width rather than the viewport’s, which finally solves component reuse across layouts.

/* mobile-first */
.grid { grid-template-columns: 1fr; }

@media (min-width: 48rem) {
  .grid { grid-template-columns: 1fr 1fr; }
}

/* container queries: respond to container, not viewport */
.card {
  container-type: inline-size;
}
@container (min-width: 320px) {
  .card-body { display: grid; grid-template-columns: 1fr 1fr; }
}

Transitions and Animations#

CSS handles most micro-animations without JavaScript. Transitions interpolate between states; @keyframes defines named animation sequences. The non-negotiable accessibility addition is prefers-reduced-motion; some users get motion-sick from animations, and the spec is the standard opt-out.

.button {
  background: var(--bg);
  transition: background 0.15s ease;
}
.button:hover { background: var(--bg-hover); }

@keyframes pulse {
  from { opacity: 1; }
  50%  { opacity: 0.5; }
  to   { opacity: 1; }
}
.loading { animation: pulse 1s infinite ease-in-out; }

Respect prefers-reduced-motion:

@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    animation-duration: 0.01ms !important;
    transition-duration: 0.01ms !important;
  }
}

Logical Properties#

Prefer logical properties for internationalization. Where margin-left is hard-coded to the visual left, margin- inline-start follows the writing direction, left in LTR languages, right in RTL languages. The same pattern applies to padding, border, and inset properties.

/* directional */
margin-inline-start: 1rem;   /* left in LTR, right in RTL */
padding-block: 1rem;
border-inline-end: 1px solid;

Strategies for Big Codebases#

How CSS scales past one stylesheet and one developer. Each strategy below addresses a different failure mode of large CSS, in global namespace collisions, runtime overhead, inconsistent design tokens, third-party style ordering. Most 2026 codebases combine several.

  • CSS Modules / scoped styles, prevent global collisions.

  • CSS-in-JS (styled-components, Emotion), co-located with components; runtime cost.

  • Utility-first (Tailwind), dominant in 2026; class names instead of stylesheets.

  • Design tokens as custom properties; theming becomes setting variables.

  • Cascade layers (@layer), predictable ordering across teams and third-party styles.