Course outline
Full-Stack course / Tools and the web

CSS fundamentals: selectors to transitions

Reading

The idea

Every page you built in the last two lessons already looks like something — plain black text on a white background, browser defaults doing all the work. CSS is the layer that takes that same HTML and decides how it's dressed: what color, what size, what happens when you point at it. Nothing about the page's meaning changes; only its appearance does, which is exactly why the last two lessons could cover structure and content without touching it.

Instead of the inline <style> block you've used so far, you'll now put your rules in their own file and connect it with one line in the <head>:

<link rel="stylesheet" href="styles.css">

<link rel="stylesheet"> tells the browser "load this file and use it as the page's styling." href points at the file, sitting next to your HTML file in the same folder. Keep using the same editor and folder habits as before — this lesson just adds one new file per page.

Selectors, specificity and the cascade

Picture an office where instructions get posted three ways: a general policy memo pinned to the main board for everyone, a memo taped to one department's door, and a sticky note stuck directly on one person's desk. If all three say something different about the same rule, the one on your actual desk wins, because it's aimed most precisely at you. That's a selector doing its job, and the office's "most specific instruction wins" rule is specificity.

p {
  color: #333;
}

.featured {
  color: #7a4a00;
}

#today-special {
  color: #b00020;
}

p is a type selector — the office-wide memo, matching every <p> on the page. .featured is a class selector — the department memo, matching every element with class="featured". #today-special is an id selector — the one sticky note, matching the single element with id="today-special", since an id must only ever appear once per page. Put all three on one paragraph (<p class="featured" id="today-special">) and the id rule wins, then the class rule, then the type rule last — not because of where they sit in the file, but because an id is a more specific target than a class, and a class is more specific than a type.

Specificity only breaks the tie when selectors actually conflict on the same property. When two rules are equally specific — say, two different class selectors both setting color on the same element — the cascade decides instead: whichever rule appears later in the file wins.

.menu-item { color: #333; }
.sold-out { color: #999; }

An element with both classes gets #999, simply because .sold-out is written second — swap the order in the file and the color swaps too. This is why a stray, overly broad selector earlier in a long stylesheet can "lose" to something you didn't expect lower down: the cascade always keeps reading to the end before it settles anything.

The box model

Every element on the page sits inside an invisible picture frame. The content is the photo itself. Around it, padding is the cream-colored mat between the photo and the frame's inner edge — it's still inside the frame, just empty space. The border is the frame itself. And margin is the gap of bare wall between this frame and whatever hangs next to it — space that belongs to the wall, not the frame.

.info-sign {
  width: 200px;
  padding: 16px;
  border: 2px solid #333;
  margin: 12px;
}

By default, that width: 200px only measures the photo — the content — and the browser adds the padding and border on top of it, so the frame actually takes up 200 + 16 + 16 + 2 + 2 = 236 pixels on the wall. That catches almost everyone off guard at least once, which is why most projects flip the rule:

* {
  box-sizing: border-box;
}

box-sizing: border-box changes what width measures: now it means the outer edge of the border, and the padding and border get squeezed inside that 200 pixels instead of added on top. The content shrinks to make room for them, but the frame's total footprint stops being a surprise.

Margin has one more quirk worth knowing: two vertical margins that touch — the bottom margin of one block and the top margin of the next — don't always add together. Depending on the elements involved, they can collapse into a single gap the size of the larger one instead of the sum of both. Giving only one side of a gap a margin, instead of both sides, sidesteps the question entirely and is the simpler habit to build.

Units, colors and typography

Think of sizing text the way a recipe sizes a pinch of salt: some amounts are a fixed measurement no matter what you're cooking, and some are relative to the size of the dish. CSS has both kinds of unit, and they behave very differently once a reader changes their browser's settings.

html {
  font-size: 100%;
}

body {
  font-size: 1rem;
  line-height: 1.5;
}

h2 {
  font-size: 1.75rem;
}

px is the fixed measuring cup — one pixel is one pixel, no matter what. rem ("root em") is relative to the root element's font size — by default that's the browser's own setting, usually 16 pixels, so 1rem is 16px and 1.75rem is 28px. The useful part: if someone bumps their browser's text size up for easier reading, every rem value scales with it, while a page built entirely in px stays frozen at the size you picked. line-height: 1.5 is itself unitless on purpose — it means "1.5 times this element's own font size," so it scales along with whatever size the text ends up at.

Color works through three interchangeable systems:

.price {
  color: #b00020;
  background-color: rgb(250, 240, 230);
}

.price-alt {
  color: hsl(350, 100%, 35%);
}

Hex (#b00020) packs red, green and blue into a six-digit code. rgb() spells the same three numbers out, 0–255 each. hsl() describes a color by hue (a position around a color wheel, 0–360), saturation (how vivid) and lightness (how close to black or white) — it's often the easiest to reason about, because "same hue, darker" is just lowering one number, where the equivalent hex or rgb change isn't obvious at a glance.

For the typeface itself, list a few fallbacks in order of preference, since not every computer has every font installed:

body {
  font-family: Georgia, "Times New Roman", serif;
}

The browser uses the first font in the list it actually has, falling all the way back to its generic serif font if none of the named ones are present.

CSS variables

A shared bulletin board can hold one sticky note that says "today's accent color: this one," with several different recipe cards around the kitchen all pointing at that same note instead of each writing their own color down separately. Change the sticky note once, and every card that points to it updates at the same time. That's what a CSS custom property — informally, a CSS variable — does.

:root {
  --accent: #7a4a00;
}

.price {
  color: var(--accent);
}

.sale-banner {
  background-color: var(--accent);
}

:root is a selector matching the page's outermost element (effectively <html>), so a variable declared there is reachable from everywhere on the page — the bulletin board in the main hallway, not a note taped inside one drawer. --accent is the variable's name; the two leading dashes are required. var(--accent) reads the value back out, wherever a normal color value would go.

Variables follow the same cascade as everything else, which means one can be re-declared in a smaller scope to override just that area:

.menu-item--special {
  --accent: #b00020;
}

Any .menu-item--special element, and anything inside it, now sees --accent as the red, while the rest of the page keeps the original brown — the sticky note gets swapped for a different one, but only inside that one drawer.

Pseudo-classes, pseudo-elements and transitions

A door can look different depending on what's happening to it right now — standing shut, being pushed open, or caught mid-swing — without being a different door. A pseudo-class targets an element based on a state it's currently in, rather than anything written in the HTML:

.menu-item:hover {
  background-color: #f5efe6;
}

.menu-item:focus-visible {
  outline: 2px solid #7a4a00;
}

.menu-item:nth-child(odd) {
  background-color: #fafafa;
}

:hover matches while a pointer sits over the element. :focus-visible matches while it holds keyboard focus and the browser judges that a visible focus ring is actually useful right now — covering the same moment :hover can't reach for anyone navigating without a mouse. :nth-child(odd) matches every other sibling by position, useful for banding a list without adding a class to half of it by hand.

A pseudo-element is different: instead of reacting to a state, it adds something to the box that isn't a separate element in your HTML at all, like screwing a nameplate onto a door that isn't listed anywhere in the building's blueprint.

.menu-item--special::before {
  content: "★ ";
  color: #b00020;
}

::before inserts generated content right at the start of the element's own content, and content: "★ " is what gets inserted — without a content value, a ::before or ::after rule does nothing at all.

None of this looks finished if it snaps between states instead of moving. A transition tells the browser to animate a property change over time instead of jumping straight to the new value:

.menu-item {
  background-color: #fff;
  transition: background-color 0.2s ease;
}

transition: background-color 0.2s ease; names the property to animate, how long it should take, and the pacing curve (ease starts and ends gently). The moment :hover changes background-color, the browser fades between the two colors over that fifth of a second instead of switching instantly.

Organizing CSS and naming

Think about labeling moving boxes before a move: "Kitchen — pots" tells you exactly where it goes and what's inside, where a box labeled "stuff" doesn't, even once it's sitting in the right room. A stylesheet earns the same kind of labeling as it grows, mostly through comments and a consistent naming habit.

/* Base */
body { font-family: Georgia, serif; color: #333; }

/* Menu */
.menu { margin: 0; padding: 0; }
.menu__item { padding: 8px 0; }
.menu__item--special { color: #b00020; }

Comments (/* like this */) split the file into labeled sections, the same way a drawer label tells you what's inside without opening it. The naming pattern here — menu, then menu__item for a part of it, then menu__item--special for a variant of that part — is one popular convention (often called BEM): a block is a component, a double underscore marks an element inside it, and a double dash marks a modifier on it. You don't have to use this exact pattern, but picking one and sticking to it means anyone reading .menu__item--special already knows it's a variant of an item inside the menu, without reading a single line of CSS.

This habit also pays off against specificity, from earlier in this lesson: a flat class like .menu__item--special has exactly the same specificity whether it's the first rule in the file or the thousandth, where a deeply nested selector chain (#page .content .menu ul li.special) keeps climbing in specificity the longer it gets, making it steadily harder to override later. Shallow, consistently named classes keep the cascade easy to predict.

Common mistakes

  • Reaching for !important to win a conflict. It overrides specificity entirely, and the next person who needs to override that has nowhere left to go but another !important. A more specific (or simply later) normal rule almost always solves it instead.
  • Forgetting box-sizing: border-box. Padding and border quietly inflate the element past the width you set, which only becomes obvious once something doesn't line up.
  • Sizing text in px out of habit. It ignores a reader's own font-size preference; rem tracks it.
  • A ::before or ::after rule with no content property. Without content, even a value of "", the pseudo-element doesn't render at all.
  • A :hover effect with no :focus-visible twin. Anyone using a keyboard instead of a mouse never sees the change.
  • Giving every element in a page the same low-effort class name, like three unrelated things all called .box. Specific, consistent names prevent rules from colliding by accident.
  • Setting a CSS variable but never reading it with var(), or vice versa — a variable only does something where it's actually referenced.

Try it

  1. Build the cascade example. Save a page with the three p/.featured/#today-special rules from the Selectors section, apply all three to one element, and confirm its final color matches the id rule. Open developer tools, select the element in Elements, and look at the Styles panel: you'll see all three rules listed, with the ones losing the cascade shown crossed out.
  2. Break the box model on purpose. Build the .info-sign example without box-sizing: border-box and check its rendered width in the Computed tab of developer tools — it will be larger than the width you set. Add box-sizing: border-box and watch the computed width drop to match.
  3. Compare units. Build the rem-based heading and paragraph from the Units section, then open the Console and run document.querySelector('h2').getBoundingClientRect().height — note the number. Change the browser's page zoom and run it again; a rem-based size changes with it.
  4. Swap a variable. Build the :root { --accent: ...; } example, apply it to two different elements with var(--accent), then change only the value in :root and reload — both update together. Add the .menu-item--special override and confirm only elements inside it pick up the new color.
  5. Trigger the states. Build the :hover, :focus-visible and ::before examples from the Pseudo-classes section. Point at the element and watch the transition ease into the hover color instead of snapping. Then click elsewhere and press Tab until keyboard focus reaches it, and confirm the focus style appears too.
  6. Reorganize a file. Take any stylesheet you've written in this lesson and split it into at least two comment-labeled sections, renaming at least one class to follow the block__element--modifier pattern from the Organizing section. Reload the page and confirm nothing about how it looks actually changed — only the file got easier to read.