CSS fundamentals every frontend developer should master

Animations are the fun part — these four foundations are what make everything else work. Each one below is interactive: drag, toggle and resize to see the concept behave, then copy the pattern.

01 · Colours

HSL: colours you can actually reason about

Hex codes like #8b5cf6 are fine for copying, but impossible to tweak by eye. HSL describes colour the way designers think: hue (position on the colour wheel), saturation (intensity) and lightness. Need a hover shade? Drop lightness by 8. Need a matching accent? Shift the hue, keep the rest. Drag the sliders — all three formats describe the identical colour.

In real projects, name your colours once as custom properties (design tokens) and reference them everywhere — change the token, and the whole site follows. That is exactly how this site is themed.

Design tokens pattern
:root {
  --brand:      hsl(258, 90%, 66%);
  --brand-dark: hsl(258, 90%, 56%);   /* hover: -10 lightness */
  --brand-soft: hsl(258 90% 66% / 0.15); /* translucent tint */
}

.button        { background: var(--brand); }
.button:hover  { background: var(--brand-dark); }
.badge         { background: var(--brand-soft); color: var(--brand); }
02 · Styling & the cascade

Specificity: why your style “doesn’t apply”

When two rules target the same element, CSS doesn’t pick the one you wrote last — it picks the most specific selector. Specificity is scored in three columns: (IDs, classes, elements), compared left to right. This is the single most common reason a style “mysteriously” fails to apply.

SelectorScoreWins?
h2(0, 0, 1)
.title(0, 1, 0)
.card .title(0, 2, 0)
#header .title(1, 1, 0)✓ highest
style="…" (inline)beats all selectorsavoid

The professional habit: keep selectors flat — one class per rule — so specificity stays low and predictable, and you never need !important to win a fight you accidentally started.

The conflict, illustrated
/* Both rules target the same link… */
#sidebar .link         { color: red; }   /* (1,1,0) — wins */
.nav .menu .item .link { color: blue; }  /* (0,4,0) — loses,
                                            despite 4 classes */

/* ✅ The maintainable style: flat, single-class selectors */
.nav-link         { color: blue; }
.nav-link.is-active { color: red; }
03 · Z-index & stacking

Why z-index: 9999 still loses

Z-index isn’t a global ranking — it only competes inside the same stacking context. A parent with a z-index, transform, filter, opacity below 1, or position: sticky creates a new context, and its children can never escape it — not even with z-index: 9999. Toggle the checkbox and watch the badge lose to a humble z-index: 1:

Panel A badge · z-index: 9999
Panel B · z-index: 1

The fix is never a bigger number on the child — it’s raising the parent’s z-index, or removing whatever created the unwanted context. (The tooltips in this site’s sidebar hit exactly this trap during development.)

The trap and the fix
/* THE TRAP */
.panel-a        { position: relative; z-index: 0; } /* new context */
.panel-a .badge { position: absolute; z-index: 9999; } /* trapped */
.panel-b        { position: relative; z-index: 1; } /* paints on top */

/* THE FIX — promote the PARENT, not the child */
.panel-a        { position: relative; z-index: 2; }
04 · Responsive design

Layouts that adapt without breakpoints

Modern responsive design is less about stacking media queries and more about letting CSS do the maths. repeat(auto-fit, minmax()) adds and removes grid columns to fit whatever space exists, and clamp() scales type fluidly between a floor and a ceiling. Drag the handle at the bottom-right corner of the box:

Fluid headline

drag ↘

Media queries still matter — write them mobile-first (base styles for small screens, then min-width enhancements upward). The animation gallery above uses exactly this auto-fit pattern, which is why it gains columns on a 2K monitor without a single extra breakpoint.

The self-adapting toolkit
/* Columns appear and disappear to fit the space */
.grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
  gap: 16px;
}

/* Type scales smoothly: never below 1.8rem, never above 3rem */
h1 { font-size: clamp(1.8rem, 4.5vw, 3rem); }

/* Mobile-first: base = small screens, enhance upward */
@media (min-width: 768px)  { /* tablets and up */ }
@media (min-width: 1600px) { /* large monitors */ }