CSS Fundamentals (Selectors, Specificity, Box Model)
The complete CSS foundation: how styling works, every selector type, the cascade & specificity, inheritance, units, colors, the box model, display & positioning, and modern layout — beginner to advanced in one page.
What is CSS? CSS (Cascading Style Sheets) is the language that controls how HTML looks — color, spacing, size, position, typography, and motion. HTML gives a page structure and meaning; CSS gives it presentation. The word Cascading is the important one: many rules can target the same element, and CSS has a precise, deterministic set of rules for deciding which one wins. Master the cascade and CSS stops feeling random.
Anatomy of a rule. Every CSS rule has two parts: a selector (which elements to style) and a declaration block (what to do to them). Inside the block are declarations, each a property: value; pair. Example: p { color: navy; font-size: 16px; } — the selector is p, and there are two declarations. Whitespace doesn't matter; the semicolons and braces do.
Three ways to apply CSS. (1) Inline — a style attribute on one element: <p style="color:red">. Highest priority, hardest to maintain, avoid it. (2) Internal — a <style> block in the <head>. Fine for a single page. (3) External — a .css file linked with <link rel="stylesheet" href="styles.css">. This is the standard: one stylesheet shared across many pages, cached by the browser.
Selectors — how you target elements. This is the vocabulary you use constantly. Type/element (p, h1) matches every element of that tag. Class (.card) matches every element with class="card" — reusable, your everyday workhorse. ID (#header) matches the single element with that id — unique per page. Universal (*) matches everything. Attribute ([type="email"], [disabled]) matches by attribute. You combine them: input.primary[required] means an <input> that has class primary AND the required attribute.
Combinators — selecting by relationship. Descendant (.card p) — any <p> anywhere inside .card. Child (.card > p) — only direct children. Adjacent sibling (h2 + p) — the <p> immediately after an <h2>. General sibling (h2 ~ p) — all <p> siblings after an <h2>. Grouping (h1, h2, h3) — apply the same rule to several selectors at once (the comma means OR).
Pseudo-classes — state and position. A pseudo-class (:) styles an element in a particular state or position without needing extra classes. State: :hover, :focus, :active, :visited, :checked, :disabled. Structural: :first-child, :last-child, :nth-child(2n) (even rows), :not(.disabled) (negation). These are dynamic — :hover applies only while the mouse is over the element.
Pseudo-elements — styling a part of an element. A pseudo-element (::, double colon) styles a sub-part: ::before and ::after inject generated content (great for icons/decoration, need a content value), ::first-line, ::first-letter, ::placeholder, ::selection. Rule of thumb: pseudo-class = a state; pseudo-element = a piece.
The cascade (how conflicts resolve). When several rules set the same property on the same element, CSS decides the winner in this order: (1) Origin & importance — an author !important beats normal author rules, which beat the browser defaults. (2) Specificity — the more specific selector wins. (3) Source order — if specificity ties, the rule that comes later wins. That's the whole algorithm. 'My style is ignored' is almost always a specificity problem, not a bug.
Specificity, precisely. Specificity is a tuple (a, b, c) you can literally count: a = number of IDs, b = number of classes + attributes + pseudo-classes, c = number of element types + pseudo-elements. Compare left to right, like version numbers. Examples: p → (0,0,1); .card → (0,1,0); #main → (1,0,0); nav ul li a → (0,0,4); .nav a:hover → (0,2,1). Inline style="" sits above all selectors, and !important sits above everything. (0,1,0) beats (0,0,4) — one class outranks any number of element selectors.
Inheritance — some properties flow down. Many text-related properties (color, font-family, font-size, line-height, text-align, visibility) are inherited: set them on a parent and children get them automatically. Most box/layout properties (width, padding, margin, border, background) are not inherited — that would be chaos. You can force it with the keyword inherit, reset with initial, or use unset (inherit if the property normally inherits, else initial).
Units — absolute vs relative. Absolute: px (one CSS pixel; predictable, most common). Relative: % (of the parent), em (relative to the element's own font-size — compounds when nested), rem (relative to the root font-size — predictable, best for type & spacing), vw/vh (1% of viewport width/height), ch (width of a 0), fr (a fraction of free space, Grid only). Prefer rem for typography and spacing so the layout respects the user's font-size preference; use px for hairline borders.
Colors. Several notations, all interchangeable: named (red), hex (#4f46e5, or #4f46e580 with alpha), rgb(79 70 229) / rgba(...) for opacity, and hsl(240 84% 59%) — Hue-Saturation-Lightness, the most human-friendly (nudge lightness to make tints/shades of the same hue). Modern CSS also has oklch() for perceptually uniform colors.
The box model — every element is a box. From the inside out: content (text/image) → padding (space inside the border, shares the background) → border → margin (transparent space outside, separates this box from others). Two gotchas: margins collapse — vertical margins between stacked block elements merge into the larger of the two, they don't add up. And by default width sets only the content width, so padding and border are added on top — a width:320px box with padding:16px and a 1px border is actually 354px wide.
box-sizing: border-box — the one fix everyone applies. Set box-sizing: border-box and width/height now include padding and border, so width:320px really renders 320px wide no matter the padding. Apply it globally (*, *::before, *::after { box-sizing: border-box; }) at the top of every project — it eliminates the constant arithmetic and is the single most useful CSS reset.
display — the box's fundamental behavior. block (starts on a new line, fills available width — <div>, <p>). inline (flows within text, ignores width/height & top/bottom margin — <span>, <a>). inline-block (flows inline but respects width/height/padding). none (removed entirely — no box, no space, excluded from layout). flex and grid turn an element into a layout container for its children (covered in depth in the next topic).
position — taking a box out of normal flow. static (default, normal flow). relative (nudged from its normal spot with top/left, but its original space is preserved — and it becomes a positioning context). absolute (removed from flow, positioned relative to the nearest positioned ancestor). fixed (positioned relative to the viewport — stays put on scroll, e.g. a sticky header). sticky (acts relative until you scroll past a threshold, then sticks). Stacking is controlled by z-index, which only works on positioned elements.
The mental model (memorise this). Selector picks the elements → the cascade + specificity decides which conflicting rule wins → inherited properties flow down the tree → the box model sizes each element → display and position place it. Everything else in CSS is a property you look up; these five ideas are the engine.
Specificity is like method overload resolution in Java — when several candidates match, the most specific signature wins, deterministically. The cascade is a priority queue: importance, then specificity, then declaration order break every tie, so nothing is ever ambiguous. Reaching for `!important` is like casting to bypass the type system — it works, but it's a code smell signalling your selectors (your 'types') are wrong. Inheritance is your DI container passing shared config down the object graph; `box-sizing: border-box` is choosing a saner default so you stop doing the same arithmetic in every constructor.
- A rule = selector + declaration block; a declaration = property: value. Prefer external stylesheets over internal, and never inline for anything reusable.
- Specificity is a countable tuple (IDs, classes/attrs/pseudo-classes, elements) compared left-to-right. (0,1,0) beats (0,0,99) — one class outranks any number of element selectors.
- The full cascade: importance/origin → specificity → source order (later wins). That resolves every conflict deterministically.
- Reaching for !important is almost always a symptom of a specificity problem — fix the selector instead.
- Text properties (color, font, line-height) inherit; box properties (width, margin, padding, border) do not. Use inherit / initial / unset to control it explicitly.
- Use rem for type & spacing (respects user font-size), px for hairline borders, % / vw / vh for fluid sizing, fr for Grid tracks.
- Box model outside-in: content → padding → border → margin. Vertical margins between block elements COLLAPSE to the larger value; they don't add.
- box-sizing: border-box on *, *::before, *::after makes width include padding + border — apply it globally, it's the most useful reset.
- display sets a box's nature (block/inline/inline-block/none/flex/grid); position (relative/absolute/fixed/sticky) takes it out of normal flow; z-index only affects positioned elements.
- Pseudo-CLASS (:hover, :nth-child) = a state or position; pseudo-ELEMENT (::before, ::first-line) = styling a part of the element.
Worked Code
/* Type / class / id / universal */
p { line-height: 1.6; } /* every <p> (0,0,1) */
.card { padding: 16px; } /* class="card" (0,1,0) */
#main { max-width: 1100px; } /* id="main" (unique) (1,0,0) */
* { margin: 0; } /* everything (0,0,0) */
/* Attribute selectors */
input[type="email"] { border-color: teal; }
a[href^="https"] { color: green; } /* starts with https */
/* Combinators */
.card p { color: #333; } /* descendant: any p inside .card */
.card > p { font-weight: 600; } /* child: direct children only */
h2 + p { margin-top: 0; } /* adjacent sibling right after h2 */
h1, h2, h3 { font-family: Georgia; } /* grouping (comma = OR) */
/* Pseudo-classes: state & structure */
a:hover { text-decoration: underline; }
input:focus { outline: 2px solid dodgerblue; }
li:nth-child(2n) { background: #f5f5f5; } /* even rows */
button:not(:disabled){ cursor: pointer; }
/* Pseudo-elements: style a PART of the element */
.quote::before { content: "\201C"; } /* injected open-quote */
p::first-line { font-weight: 700; }
::selection { background: gold; }/* Same element <p id="lead" class="intro">, four rules target it. */
p { color: gray; } /* (0,0,1) */
.intro { color: blue; } /* (0,1,0) */
#lead { color: green; } /* (1,0,0) <-- WINS on specificity */
p.intro { color: teal; } /* (0,1,1) loses to the id above */
/* Source order breaks a TIE (same specificity) */
.btn { background: navy; }
.btn { background: crimson; } /* later wins -> crimson */
/* !important overrides specificity (use sparingly) */
.alert { color: red !important; } /* beats even an inline style *//* The reset every project should start with */
*, *::before, *::after { box-sizing: border-box; margin: 0; }
.card {
width: 320px; /* WITH border-box this is the true rendered width */
padding: 16px; /* space inside the border */
border: 1px solid #ddd;
margin: 12px; /* space outside; collapses vertically with neighbours */
border-radius: 8px;
}
/* Without border-box the same box would render 320 + 32 + 2 = 354px wide.
With border-box it renders exactly 320px. That predictability is the point. */:root { font-size: 16px; } /* 1rem = 16px everywhere */
body {
color: #222; /* inherited by all descendants */
font-family: system-ui, sans-serif; /* inherited */
line-height: 1.6; /* inherited */
}
.spacer { margin-block: 1.5rem; } /* rem: scales with root font-size */
.hero { min-height: 60vh; } /* vh: 60% of viewport height */
/* position: fixed header that stays on scroll, above other content */
.header {
position: fixed;
top: 0; left: 0; right: 0;
z-index: 100; /* z-index needs a positioned element */
}
/* position: absolute badge, anchored to its relatively-positioned card */
.card { position: relative; }
.card .badge{ position: absolute; top: 8px; right: 8px; }▶Try It Live
Edit the code and press Run — it executes safely in a sandboxed iframe. Use the Console tab for log output.
Interview-Ready Q&A
Specificity is a tuple (IDs, classes/attributes/pseudo-classes, type/pseudo-elements) — count each and compare left to right like version numbers. Inline styles sit above all selectors, and !important sits above everything. If two rules have equal specificity, source order decides — the later rule wins. So the full resolution order is: importance/origin → specificity → source order.
- 1Rule = selector + { property: value; }. Prefer external stylesheets; avoid inline styles.
- 2Cascade order: importance/origin → specificity → source order (later wins).
- 3Specificity tuple: (IDs, classes/attrs/pseudo-classes, elements). One class (0,1,0) beats any number of elements.
- 4!important is a red flag — fix the selector instead of escalating.
- 5Text props inherit (color, font, line-height); box props don't (width, margin, padding).
- 6Box model outside-in: content → padding → border → margin. Vertical margins collapse.
- 7Always set box-sizing: border-box globally.
- 8Units: rem for type/spacing, px for borders, % / vw / vh for fluid sizing, fr for Grid.
- 9position: relative (context, keeps space) / absolute (nearest positioned ancestor) / fixed (viewport) / sticky (relative→fixed). z-index needs positioning.
- 10Pseudo-class (:hover) = state; pseudo-element (::before) = a part.