🎨

A language tour

The Art of CSS

Misunderstood as decoration. Actually a constraint-solving layout engine, a cascade algebra, and an animation system — all in one.

scroll

01 — The Cascade

Where every style comes from

CSS stands for Cascading Style Sheets. The cascade is the algorithm that decides which rule wins when multiple rules target the same element — weighing origin, specificity, and order. Understanding it ends the "why won't this work" frustration permanently.

"The cascade is not a bug, not a quirk, and not something to fight. It's the most powerful feature in the language — once you understand it."

— Every senior CSS developer, eventually
cascade.css
/* Specificity: inline > ID > class > element */
p           { color: black; }   /* 0-0-1 */
.note       { color: grey; }    /* 0-1-0 */
#intro      { color: navy; }    /* 1-0-0  ← wins */

/* Inheritance — children inherit text properties from parents */
body {
  font-family: Georgia, serif;
  line-height: 1.6;
  /* every element inherits this — you set it once */
}

/* The :is() selector reduces specificity noise */
:is(h1, h2, h3) {
  font-weight: 700;
  line-height: 1.2;
}

/* Layers — explicit control over the cascade order */
@layer reset, base, components, utilities;

@layer utilities {
  .visually-hidden {
    position: absolute;
    width: 1px;
    clip: rect(0 0 0 0);
  }
}

@layer (CSS 2022) lets you declare the cascade order upfront — utilities always beat components, components always beat base — no more specificity wars.


02 — Custom Properties

Variables that know where they are

CSS custom properties aren't just variables — they're scoped, inheritable, and live values that update in real time. A component can override a property defined on the root, and every child of that component sees the override. This is how design tokens work.

custom-properties.css
/* Define at root — available everywhere */
:root {
  --color-accent:  #264DE4;
  --color-surface: #F5F7FF;
  --spacing-base:  1rem;
  --radius:         8px;
}

/* Dark mode — override the same variables, nothing else changes */
@media (prefers-color-scheme: dark) {
  :root {
    --color-accent:  #6B8FFF;
    --color-surface: #090C1E;
  }
}

/* Component-level override — scoped to .card and its children */
.card {
  --radius: 16px;
  background: var(--color-surface);
  border-radius: var(--radius);
  padding: calc(var(--spacing-base) * 1.5);
}

/* With a fallback — graceful when undefined */
.badge {
  color: var(--badge-color, var(--color-accent));
}

Dark mode with zero JavaScript — redefine the same custom properties inside a @media (prefers-color-scheme: dark) block and the entire theme inverts.


03 — Grid Layout

Two-dimensional layout finally solved

CSS Grid is the layout system the web always needed. Before it, developers built grids with floats, tables, flexbox workarounds — each a clever hack. Grid was designed specifically for two-dimensional layout and it shows: the code describes what you want, not how to fake it.

grid.css
/* Named areas — the layout written as a picture */
.page {
  display:               grid;
  grid-template-columns: 240px 1fr;
  grid-template-rows:    auto 1fr auto;
  grid-template-areas:
    "header  header"
    "sidebar main  "
    "footer  footer";
  min-height: 100vh;
}

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

/* Auto-fit responsive grid — no media queries needed */
.card-grid {
  display:               grid;
  grid-template-columns: repeat(auto-fit, minmax(260px, 1fr));
  gap:                   1.5rem;
  /* cards reflow from 1 → 2 → 3 columns automatically */
}

/* Subgrid — children share the parent's grid lines */
.card {
  display:     grid;
  grid-row:    span 3;
  grid-template-rows: subgrid;  /* align across cards */
}

repeat(auto-fit, minmax(260px, 1fr)) — three columns on desktop, two on tablet, one on mobile, with zero media queries. Grid figures it out.


04 — Selectors

The query language hiding in plain sight

CSS selectors are a mini query language for the document. Modern selectors can express things like "the last three items in a list of more than five" or "any paragraph that doesn't contain a link" — without JavaScript touching the DOM.

selectors.css
/* :has() — the parent selector CSS never had, until now */
.card:has(img) {
  padding-top: 0;  /* card with an image gets no top padding */
}

form:has(:invalid) {
  border-color: red;  /* form turns red if any field is invalid */
}

/* :nth-child with an expression */
li:nth-child(3n + 1) { color: blue; }  /* 1st, 4th, 7th… */

/* :not() with a complex selector */
p:not(.lead):not(:last-child) {
  margin-bottom: 1rem;
}

/* Attribute selectors — pattern match on HTML attributes */
a[href^="https"]  { color: green; }  /* starts with https */
a[href$=".pdf"]   { color: red; }    /* ends with .pdf */
a[href*="github"] { color: purple; } /* contains github */

/* :where() — zero specificity, overridable anywhere */
:where(article, section) p {
  max-width: 65ch;
}

form:has(:invalid) styles the parent based on a child's state — something JavaScript developers wrote utility functions for. Now it's a single CSS selector.


05 — Animations & Transitions

Motion design in plain text

CSS can animate almost any visual property — position, size, colour, opacity, shape — without a single line of JavaScript. Transitions handle the simple case. Keyframes handle the complex. And animation-timeline ties motion to the scroll position itself.

animations.css
/* Transition — smooth change between states */
.button {
  background:  var(--color-accent);
  transition:  background 0.2s ease, transform 0.15s ease;
}
.button:hover {
  background:  oklch(from var(--color-accent) calc(l + 0.1) c h);
  transform:   translateY(-2px);
}

/* Keyframes — full choreography */
@keyframes fadeSlideIn {
  from { opacity: 0; transform: translateY(20px); }
  to   { opacity: 1; transform: translateY(0); }
}

.hero-title {
  animation: fadeSlideIn 0.7s cubic-bezier(0.22, 1, 0.36, 1) forwards;
}

/* Scroll-driven animation — no JavaScript whatsoever */
@keyframes reveal {
  from { opacity: 0; transform: scale(0.95); }
  to   { opacity: 1; transform: scale(1); }
}

.card {
  animation:          reveal linear both;
  animation-timeline: view();          /* tied to scroll position */
  animation-range:    entry 0% entry 40%;
}

animation-timeline: view() links an animation directly to the element's position in the viewport — scroll-driven reveals with zero JavaScript.


06 — The Whole Picture

More reasons CSS rewards patience

📐

Container Queries

Style a component based on its own container's size — not the viewport. Components finally become truly self-contained.

🎨

oklch() Colour

A perceptually uniform colour space. Equal steps in lightness actually look equal. Generate accessible colour palettes with a formula, not a guess.

📝

Logical Properties

margin-inline-start instead of margin-left — layout that adapts to writing direction automatically. RTL support for free.

🔡

Variable Fonts

One font file with axes for weight, width, and optical size. Animate typography. Stop downloading 8 font files for one typeface.

🔲

Nesting

Native CSS nesting arrived in 2023. Write .parent { & .child { } } without a preprocessor — Sass-style structure in plain CSS.

🌀

The View Transition API

Animate between pages or states with CSS keyframes. Full-page transitions that used to require a JavaScript framework, now native.