Step 03 of 21
CSS fundamentals
Target elements with selectors, predict which rule wins using the cascade and specificity, and size boxes with confidence.
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.
/* selector { property: value; } */
.card-title {
color: #1d3557;
font-size: 1.25rem;
}There are three ways to add CSS to a page:
<!-- 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:
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:
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:
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:
- Origin and importance. Your stylesheets beat the browser's default styles. A declaration marked
!importantbeats normal declarations. - Inline styles. A
style="…"attribute beats any selector. - Cascade layers. If you use
@layer, rules in later layers beat earlier ones. - Specificity. The more specific selector wins.
- Order of appearance. If everything else ties, the rule that comes last wins.
Order of appearance is the one you hit first:
.btn { background: steelblue; }
.btn { background: seagreen; } /* wins: same specificity, comes later */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
<article class="card">
<h2 id="headline" class="title">New season</h2>
</article>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:
.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:
┌─────────────── 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:
.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:
*, *::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.
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.
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.widthandheightare 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:
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.
Practice
Create a file with this HTML and CSS. Before opening it, predict the answers to the questions below, then check them in DevTools.
<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>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; }- What colour is the heading?
- If you delete the
#shop h2rule, what colour is it? - What colour is the paragraph? And the link?
- What is the rendered width of the card? What is it after adding the
border-boxreset?
Show solution
- Purple. Specificities are
h20,0,1;.card h20,1,1;.featured .card-title0,2,0;#shop h21,0,1;.card-title0,1,0. The ID wins, even though.card-titlecomes last. - 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.
- The paragraph is #333, inherited from
.card. The link is the browser's default link colour (blue, or purple once visited): the browser's ownarule targets it directly, which beats an inherited value. Add.card a { color: inherit; }to change that. 20remis 320px. Withcontent-box: 320 + 24 + 24 + 2 + 2 = 372px. Withborder-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-boxsowidthmeans what you expect. - Vertical margins between blocks collapse to the larger value, but not inside flex or grid.
Check yourself
Quiz
0/4 correct · 4 left
1.Which selector has the highest specificity?
2.Two rules, .btn { color: white; } and .btn { color: black; }, appear in that order in the same stylesheet. What colour is the button text?
3.An element has width: 200px; padding: 10px; border: 5px solid; and the default box-sizing. How wide is it on screen, excluding margin?
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?
Concepts in this lesson
One-page notes on each idea, for review or a quick lookup later.
Finished reading?
Mark it complete to fill in your roadmap.