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.
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.
Selector
Score
Wins?
h2
(0, 0, 1)
—
.title
(0, 1, 0)
—
.card .title
(0, 2, 0)
—
#header .title
(1, 1, 0)
✓ highest
style="…" (inline)
beats all selectors
avoid
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.
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; }
How to use these animations
Copy the CSS. Click any card’s Copy CSS button — the snippet contains the class and its keyframes, fully self-contained.
Paste it into your stylesheet. All classes are prefixed fh-, so they won’t collide with your existing styles. Rename them freely.
Add the class to your element. The card’s Code view shows a ready-made HTML usage example for every effect.
Respect users who prefer less motion. Add this once, globally, and every animation on your site will settle instantly for people who have reduced motion enabled in their OS settings:
Reduced-motion safety net
Why these animations are fast: transform and opacity
Almost every effect in this library animates only two properties: transform and opacity. Browsers can hand these off to the GPU compositor, which means the animation runs on its own thread at 60fps even while JavaScript is busy. Animating layout properties like width, height, top or margin forces the browser to recalculate layout on every frame — the main cause of janky, stuttering motion on mid-range phones.
That is also why a pure-CSS approach beats pulling in a JavaScript animation library for simple UI motion: zero bundle weight, no main-thread work, and the browser can optimise the whole animation ahead of time. Save JavaScript animation for things CSS genuinely cannot express, such as physics, gestures, or interruptible timelines.
When (and when not) to animate
Good UI motion answers a question the user already has: where did this come from, what changed, what can I do next? Entrance animations orient the eye when content appears; hover effects confirm that an element is interactive; loaders reassure users that work is happening. Used this way, motion measurably improves perceived performance and task confidence.
The failure mode is decoration: animating everything, everywhere, on every scroll. Keep durations between 150ms and 600ms for interface feedback, reserve longer, looping effects for ambient backgrounds, and never gate a user’s next action behind an animation they must wait for. One well-placed effect reads as polish; ten read as noise.
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.