CSS fundamentals every frontend developer should master

Animations are the fun part — these seven foundations are what make everything else work. Each one below is interactive: drag, toggle, scroll 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 */ }
05 · Themed Scrollbars

Custom scrollbars: the detail that completes a theme

A dark, carefully-themed site with a default light-grey scrollbar is a suit with trainers — the browser default breaks the illusion on the most-used control on the page. Two systems exist for styling them, and the trick is knowing how they interact. Every scrollbar on this site — the page itself, code blocks, this demo box — uses exactly the CSS below.

Scroll me — this box, and the whole page, use the themed scrollbar.

The ::-webkit-scrollbar family of pseudo-elements works in Chrome, Edge, Safari and every Chromium browser, and gives you full control: width, track, thumb, gradients, rounded corners, hover states.

Firefox instead implements the standard properties: scrollbar-width (auto, thin, none) and scrollbar-color (thumb colour, track colour). Simpler — no gradients — but perfectly themeable.

The gotcha: Chrome 121+ supports the standard properties too, and if you set scrollbar-color there, it switches the scrollbar to basic mode and ignores all your ::-webkit-scrollbar styling.

The fix is the @supports not selector(::-webkit-scrollbar) guard below: fancy pseudo-elements where they're supported, standard properties everywhere else. Each browser gets the best version it can render.

One accessibility rule: theme scrollbars, don't hide them. A thumb needs enough contrast against its track to be findable, and scrollbar-width: none on a scrollable region hides the only visual clue that there's more content.

Themed scrollbars — the complete pattern
/* Chrome, Edge, Safari — full custom look */
::-webkit-scrollbar {
  width: 11px;                      /* vertical bar */
  height: 11px;                     /* horizontal bar */
}
::-webkit-scrollbar-track {
  background: rgba(255, 255, 255, 0.03);
  border-radius: 8px;
}
::-webkit-scrollbar-thumb {
  background: linear-gradient(180deg, #8b5cf6, #06b6d4);
  border-radius: 8px;
  border: 2px solid #060913;        /* match your page bg — */
  background-clip: padding-box;     /* creates a slim gap    */
}
::-webkit-scrollbar-thumb:hover {
  background: linear-gradient(180deg, #9d74f8, #2fc9e8);
  border: 2px solid #060913;
  background-clip: padding-box;
}
::-webkit-scrollbar-corner { background: transparent; }

/* Firefox (and anything without webkit scrollbars).
   Scoped with @supports so it can't override the fancy
   version in Chrome 121+, which supports BOTH systems
   and prefers this one when set. */
@supports not selector(::-webkit-scrollbar) {
  * {
    scrollbar-width: thin;
    scrollbar-color: #8b5cf6 rgba(255, 255, 255, 0.05);
  }
}

Bare ::-webkit-scrollbar selectors apply to the page and every scrollable element at once. To style just one container, prefix them: .code-box::-webkit-scrollbar-thumb { … }.

06 · Theme Tokens

Design tokens: theme switching in one attribute

Hard-coded colours are why dark mode retrofits take weeks. The professional pattern: name every colour once as a custom property on :root, reference only the tokens everywhere else, and switching themes becomes swapping one data-theme attribute. This entire site is built on tokens — try it live:

Token-driven card

Every colour here comes from four variables.

The theme-switching pattern
/* 1. Name the colours ONCE — the tokens */
:root {
  --bg: #0b1224;
  --surface: #141c30;
  --text: #e2e8f0;
  --accent: #06b6d4;
}

/* 2. Each theme just re-assigns the same tokens */
[data-theme="light"] {
  --bg: #f1f5f9;
  --surface: #ffffff;
  --text: #1e293b;
  --accent: #7c3aed;
}

/* 3. Components only ever reference tokens */
body    { background: var(--bg); color: var(--text); }
.card   { background: var(--surface); }
.button { background: var(--accent); }

/* JS: 3 lines to switch and remember the choice */
// document.documentElement.dataset.theme = 'light';
// localStorage.setItem('theme', 'light');
// on load: dataset.theme = localStorage.getItem('theme') || '';

Respect the visitor's OS preference as the default with @media (prefers-color-scheme: light), and let the toggle override it. Pair with color-scheme (see lesson 05) so native controls follow your theme too.

07 · Positioning

Relative vs absolute: who positions whom

The rule that unlocks all of CSS positioning: absolute positions an element against its nearest positioned ancestor — and position: relative on the parent is how you volunteer it for that job. Relative also lets an element nudge from its own spot while keeping its space in the flow; absolute removes it from the flow entirely. Watch the siblings:

.parent — position: relative
sibling before
.box
sibling after

static — the default: the box sits in the normal flow; top/left do nothing.

sticky is the hybrid: it scrolls normally until it reaches your offset, then pins — the header on this site and the bar in this box both use it:

position: sticky; top: 0 — I pin while my parent scrolls

Scroll this box. The bar above stays pinned to the top of the container while this content moves underneath it.

Sticky needs two things to work: an offset (top: 0) and a scrollable ancestor taller than the element.

Classic uses: table headers, section labels in long lists, the toolbar on this very page.

If sticky "doesn't work", check that no ancestor has overflow: hidden — that silently disables it.

The positioning cheat sheet
/* The pattern behind 90% of overlays, badges and tooltips */
.parent {
  position: relative;   /* becomes the anchor — moves nothing */
}
.badge {
  position: absolute;   /* leaves the flow entirely */
  top: -8px;
  right: -8px;          /* measured from .parent's corner */
}

/* relative: nudge visually, keep your reserved space */
.nudge { position: relative; top: 4px; left: 8px; }

/* fixed: pinned to the viewport, ignores scrolling */
.cookie-banner { position: fixed; bottom: 20px; left: 20px; }

/* sticky: in-flow until it hits the offset, then pins */
.table-header { position: sticky; top: 0; }

Frequently asked questions

Can I use these CSS animations in commercial projects?

Yes — every animation in the library is free for personal and commercial use, with no attribution required. Copy the CSS, rename the classes if you like, and ship it.

Do these animations need JavaScript?

No. Every effect is pure CSS — including the accordion, carousel, dropdown menus and hamburger toggle, which use native elements and modern selectors instead of scripts. JavaScript is only ever suggested for triggering (adding a class on scroll or on error).

Will CSS animations slow my website down?

Not when they animate the right properties. Almost every effect here animates only transform, opacity or filter, which browsers composite on the GPU off the main thread — zero bundle weight and no jank, unlike JavaScript animation libraries.

Do the animations respect reduced-motion settings?

The page previews pause automatically when your OS prefers reduced motion, and the how-to guide ships a copyable prefers-reduced-motion safety net you can add once to your own site.