# CSS fundamentals

> Target elements with selectors, predict which rule wins using the cascade and specificity, and size boxes with confidence.

Frontend Developer roadmap · The web platform · 16 min read · https://levelcode.org/roadmaps/frontend/css-fundamentals

Most CSS frustration comes from one question: "why isn't my style applying?" The answer is almost always found in three ideas – which elements a selector matches, how the cascade decides between competing rules, and how the box model sizes each element. Once you understand those, CSS stops feeling random. This lesson covers all three, with enough worked examples that you can predict the result before you open the browser.

## How CSS attaches to HTML

A stylesheet is a list of **rules**. Each rule has a **selector** (which elements) and a block of **declarations** (what to do to them). Each declaration is a **property** and a **value**.

```css
/* selector    { property: value; } */
.card-title {
  color: #1d3557;
  font-size: 1.25rem;
}
```

There are three ways to add CSS to a page:

```html
<!-- 1. External stylesheet: the normal choice, cached across pages -->
<link rel="stylesheet" href="/styles.css">

<!-- 2. A <style> element in the head: fine for small demos -->
<style>
  body { font-family: system-ui, sans-serif; }
</style>

<!-- 3. Inline style attribute: overrides almost everything, hard to maintain -->
<p style="color: crimson;">Warning</p>
```

Use external stylesheets for real projects. Inline styles are hard to override and impossible to reuse.

## Selectors: choosing what to style

### The basic selectors

| Selector | Matches | Example |
| --- | --- | --- |
| `p` | Every `<p>` element (type selector) | `p { line-height: 1.6; }` |
| `.price` | Every element with `class="price"` | `.price { font-weight: 700; }` |
| `#cart` | The element with `id="cart"` | `#cart { position: sticky; }` |
| `[type="email"]` | Elements with that attribute value | `input[type="email"] { … }` |
| `*` | Every element | `* { margin: 0; }` |

An element can have many classes – `class="btn btn-primary"` – and you can require several at once with `.btn.btn-primary` (no space).

### Combinators

Combinators select elements based on their position relative to others:

```css
article p      { }  /* any <p> inside an <article>, at any depth (descendant) */
ul > li        { }  /* <li> that are direct children of a <ul> (child) */
h2 + p         { }  /* the <p> immediately after an <h2> (next sibling) */
h2 ~ p         { }  /* every <p> after an <h2> with the same parent */
```

### Pseudo-classes and pseudo-elements

**Pseudo-classes** (one colon) match elements in a particular state or position:

```css
a:hover            { text-decoration: underline; }
button:focus-visible { outline: 3px solid orange; }
input:invalid      { border-color: crimson; }
li:first-child     { font-weight: 700; }
tr:nth-child(even) { background: #f4f4f4; }
.btn:not(.primary) { background: white; }
```

`:focus-visible` matches when the browser decides a focus ring should be shown (typically keyboard focus), so you can style focus without showing rings on every mouse click.

**Pseudo-elements** (two colons) style a part of an element, or insert generated content:

```css
p::first-line   { font-variant: small-caps; }
.required::after { content: " *"; color: crimson; }
```

### Grouping and :is()

A comma groups selectors that share declarations: `h1, h2, h3 { font-family: Georgia, serif; }`. The `:is()` pseudo-class does the same thing inside a larger selector: `:is(article, aside) h2` matches an `<h2>` inside either.

## The cascade: who wins when rules conflict

Several rules often set the same property on the same element. The **cascade** is the algorithm that picks a winner. It checks these criteria in order, and only moves to the next one when there is a tie:

1. **Origin and importance.** Your stylesheets beat the browser's default styles. A declaration marked `!important` beats normal declarations.
2. **Inline styles.** A `style="…"` attribute beats any selector.
3. **Cascade layers.** If you use `@layer`, rules in later layers beat earlier ones.
4. **Specificity.** The more specific selector wins.
5. **Order of appearance.** If everything else ties, the rule that comes last wins.

Order of appearance is the one you hit first:

```css
.btn { background: steelblue; }
.btn { background: seagreen; }  /* wins: same specificity, comes later */
```

> [!NOTE]
> Cascade layers let you control priority explicitly: `@layer reset, base, components;` declares the order, and any rule in `components` beats any rule in `reset`, regardless of specificity. Styles not in any layer beat all layered styles. You will mostly meet layers in frameworks and design systems.

## Specificity

Specificity is a score with three columns, compared left to right like version numbers. Count:

- **A** – ID selectors (`#cart`)
- **B** – classes, attribute selectors and pseudo-classes (`.price`, `[type="email"]`, `:hover`)
- **C** – type selectors and pseudo-elements (`p`, `::before`)

The universal selector `*` and combinators add nothing.

| Selector | Specificity (A,B,C) |
| --- | --- |
| `p` | 0,0,1 |
| `p.note` | 0,1,1 |
| `nav a:hover` | 0,1,2 |
| `input[type="email"]` | 0,1,1 |
| `.card .title` | 0,2,0 |
| `#intro p` | 1,0,1 |
| `:is(#intro, .lead) p` | 1,0,1 |
| `:where(#intro, .lead) p` | 0,0,1 |

`:is()` and `:not()` take the specificity of their most specific argument. `:where()` always contributes zero, which makes it ideal for defaults that should be easy to override.

### A worked example

```html
<article class="card">
  <h2 id="headline" class="title">New season</h2>
</article>
```

```css
h2           { color: black; }   /* 0,0,1 */
.card h2     { color: navy; }    /* 0,1,1 */
.title       { color: teal; }    /* 0,1,0 */
article .title { color: purple; } /* 0,1,1 */
```

Two selectors tie at 0,1,1 – `.card h2` and `article .title` – and both beat the others. They tie on specificity, so order decides: `article .title` comes later and the heading is **purple**. Add `#headline { color: red; }` anywhere in the file and red wins at 1,0,0, because one ID outranks any number of classes.

### Keeping specificity under control

Specificity wars – adding IDs and `!important` to beat earlier rules – make stylesheets brittle. Prefer single class selectors for components, avoid IDs in CSS, and reserve `!important` for utilities that must always apply.

## Inheritance

Some properties pass from parent to child automatically. Set `color` on `<body>` and every paragraph inherits it. Mostly, **text properties inherit** (`color`, `font-family`, `font-size`, `line-height`, `text-align`) and **box properties do not** (`margin`, `padding`, `border`, `width`, `background`).

An inherited value is the weakest thing there is: *any* rule that targets the element directly beats it, even the browser's default styles. That is why links stay blue inside a `.card { color: #333; }` – the browser's stylesheet sets a colour on `a` directly. You can force inheritance with the `inherit` keyword:

```css
.card a { color: inherit; }
```

Other global keywords are `initial` (the property's default per the specification) and `unset` (inherit if the property normally inherits, otherwise `initial`).

## The box model

Every element generates a rectangular box made of four layers, from the inside out:

```text
┌─────────────── margin ───────────────┐
│ ┌──────────── border ──────────────┐ │
│ │ ┌───────── padding ────────────┐ │ │
│ │ │          content             │ │ │
│ │ └──────────────────────────────┘ │ │
│ └──────────────────────────────────┘ │
└──────────────────────────────────────┘
```

- **Content** – text, images, child elements.
- **Padding** – space inside the border; it shows the background.
- **Border** – a line around the padding.
- **Margin** – transparent space outside the border, pushing other boxes away.

### box-sizing

By default (`box-sizing: content-box`), `width` sets only the content width. Padding and border are added on top:

```css
.panel {
  width: 300px;
  padding: 20px;
  border: 2px solid;
}
/* rendered width: 300 + 20 + 20 + 2 + 2 = 344px */
```

With `box-sizing: border-box`, `width` includes padding and border, so the panel is exactly 300px and the content shrinks to 256px. That is far easier to reason about, so most projects start with:

```css
*, *::before, *::after {
  box-sizing: border-box;
}
```

Try it: switch `box-sizing` and drag the sliders. Watch which number stays fixed and which one absorbs the padding and border.

> **Interactive: The box model, live.** Margin is outside the border and never part of the element's own size. With border-box, the content area shrinks to make room for padding and border. Try it: https://levelcode.org/labs/box-model

### Block and inline boxes

The `display` property controls how a box participates in layout:

- **`block`** (`<div>`, `<p>`, `<h1>`, `<section>`): starts on a new line and fills the available width. Width, height, margin and padding all work as expected.
- **`inline`** (`<span>`, `<a>`, `<strong>`): flows within a line of text. `width` and `height` are ignored, and vertical margins do not push other lines away.
- **`inline-block`**: flows inline but respects width, height and vertical spacing – useful for small badges.

### Margin collapsing

When two block elements' vertical margins touch, they **collapse** into one margin the size of the larger:

```css
h2 { margin-bottom: 24px; }
p  { margin-top: 16px; }
/* gap between them: 24px, not 40px */
```

A child's top margin can also collapse through a parent that has no padding or border, so the margin appears *outside* the parent. Collapsing never happens inside flex or grid containers, and never horizontally.

> [!TIP]
> Open DevTools, select an element and look at the box-model diagram in the Styles or Computed pane. It shows the exact content, padding, border and margin, and the Styles pane shows overridden declarations struck through – the fastest way to answer "why isn't my style applying?"

## Practice

Create a file with this HTML and CSS. **Before** opening it, predict the answers to the questions below, then check them in DevTools.

```html
<main id="shop">
  <article class="card featured">
    <h2 class="card-title">Ridge Runner</h2>
    <p>Light and waterproof.</p>
    <a href="/buy">Buy now</a>
  </article>
</main>
```

```css
html { font-size: 16px; }
.card { color: #333; width: 20rem; padding: 1.5rem; border: 2px solid; }
h2 { color: black; }
.card h2 { color: navy; }
.featured .card-title { color: orange; }
#shop h2 { color: purple; }
.card-title { color: green; }
```

1. What colour is the heading?
2. If you delete the `#shop h2` rule, what colour is it?
3. What colour is the paragraph? And the link?
4. What is the rendered width of the card? What is it after adding the `border-box` reset?

**Solution**

1. **Purple.** Specificities are `h2` 0,0,1; `.card h2` 0,1,1; `.featured .card-title` 0,2,0; `#shop h2` 1,0,1; `.card-title` 0,1,0. The ID wins, even though `.card-title` comes last.
2. **Orange.** 0,2,0 beats 0,1,1 and 0,1,0, because the B column is compared before C. Order does not matter here because specificity is not tied.
3. The paragraph is **#333**, inherited from `.card`. The link is the **browser's default link colour** (blue, or purple once visited): the browser's own `a` rule targets it directly, which beats an inherited value. Add `.card a { color: inherit; }` to change that.
4. `20rem` is 320px. With `content-box`: 320 + 24 + 24 + 2 + 2 = **372px**. With `border-box`: exactly **320px**, and the content area becomes 320 − 48 − 4 = 268px.

## Key takeaways

- A rule is a selector plus declarations; prefer external stylesheets and class selectors.
- The cascade checks origin and importance, inline styles, layers, specificity, then order of appearance.
- Specificity is compared column by column: IDs, then classes/attributes/pseudo-classes, then types. `:where()` adds zero.
- Text properties inherit; box properties do not. Any direct rule beats an inherited value.
- Boxes are content, padding, border and margin; use `box-sizing: border-box` so `width` means what you expect.
- Vertical margins between blocks collapse to the larger value, but not inside flex or grid.

## Quiz

1. Which selector has the highest specificity?

   A. .nav .item .link
   B. #menu a
   C. ul li a:hover
   D. nav > ul > li > a

   **Answer: B.** #menu a scores 1,0,1. .nav .item .link is 0,3,0, ul li a:hover is 0,1,3 and nav > ul > li > a is 0,0,4. A single ID outranks any number of classes.

2. Two rules, .btn { color: white; } and .btn { color: black; }, appear in that order in the same stylesheet. What colour is the button text?

   A. White, because it was declared first
   B. It depends on the HTML attribute order
   C. The browser default, because the rules cancel out
   D. Black, because it comes later with equal specificity

   **Answer: D.** When origin, layers and specificity tie, order of appearance decides and the later declaration wins.

3. An element has width: 200px; padding: 10px; border: 5px solid; and the default box-sizing. How wide is it on screen, excluding margin?

   A. 200px
   B. 215px
   C. 230px
   D. 220px

   **Answer: C.** With content-box, padding and border are added to the width: 200 + 10 + 10 + 5 + 5 = 230px. With border-box it would be 200px.

4. A <p> with margin-bottom: 30px is followed by a <p> with margin-top: 20px in normal block flow. How much space is between them?

   A. 30px
   B. 50px
   C. 20px
   D. 10px

   **Answer: A.** Adjacent vertical margins collapse into a single margin equal to the larger one, 30px. They would add up to 50px inside a flex or grid container, where margins do not collapse.
