CSS
Layout and presentation: the cascade, the box model, Flexbox and Grid, responsive design, custom properties and motion.
How CSS Decides
CSS is a system for resolving conflicts. Many rules can target the same element; the language defines exactly which one applies. Learn that resolution order and the language stops feeling arbitrary.
The browser asks, in order: does a rule match this element? If several do, which has the highest specificity? If they tie, which came last? That is nearly the whole story, and the section below is the part everyone skips.
Selectors, Cascade & Specificity
Specificity is counted as three numbers: ids, then classes (including attribute selectors and pseudo-classes), then elements. Compare left to right; the first difference decides it. No amount of classes ever beats a single id.
| Selector | Specificity | Reads as |
|---|---|---|
p | 0-0-1 | One element |
.card | 0-1-0 | One class |
.card p | 0-1-1 | Class + element |
.card.dark p | 0-2-1 | Two classes + element |
#hero | 1-0-0 | Beats every combination of classes |
style="…" | inline | Beats all selectors |
Two rules tie? The later one in source order wins. This is why a media query written above the rule it means to override silently does nothing — a mistake that survives review because the code looks right.
!important is not a strength level, it is a separate layer that ignores specificity. It wins now and costs you later, because the only way to beat it is another !important. Reach for it in third-party overrides, not in your own stylesheet.Inheritance is separate from the cascade. Font and colour properties pass down to children; layout properties like margin and display do not. inherit, initial and unset let you opt in or out per property.
The Box Model & Positioning
Every element is a box: content, then padding, then border, then margin. The argument is over what width measures.
/* width now includes padding and border, not just content */ *, *::before, *::after { box-sizing: border-box; } /* without it, this box is 340px wide, not 300 */ .card { width: 300px; padding: 16px; border: 4px solid; }
Vertical margins between siblings collapse — 20px below one element and 30px above the next produces 30px, not 50. It surprises everyone once. Padding and flex or grid gaps do not collapse, which is one reason gap has largely replaced margin for spacing inside a layout.
position | Positioned relative to | Leaves a gap behind? |
|---|---|---|
static | Normal flow (the default) | — |
relative | Where it would have been | Yes, space is kept |
absolute | Nearest positioned ancestor | No, removed from flow |
fixed | The viewport | No |
sticky | Flow until a scroll threshold | Yes |
position: absolute looks for the nearest ancestor that is not static. Forgetting to set position: relative on that ancestor is why the element flew to the corner of the page.Flexbox
Flexbox lays out in one direction and distributes leftover space. Reach for it for a row of buttons, a navigation bar, a card footer — anything essentially linear.
.bar { display: flex; align-items: center; /* across the line */ justify-content: space-between; /* along the line */ gap: 12px; flex-wrap: wrap; /* let it break rather than overflow */ } .bar .spacer { flex: 1; } /* grow, shrink, basis 0 */
The two axes are the thing to memorise: justify-content works along the direction of flow, align-items works across it. Set flex-direction: column and the two swap meaning, which is where most confusion comes from.
flex: 1 is shorthand for flex: 1 1 0 — grow to fill, shrink if needed, ignore my content width when dividing space. It is what makes equal columns equal.Grid
Grid lays out in two directions at once. Use it for page structure and card galleries — anywhere rows and columns both matter. Flexbox and Grid are not rivals; a Grid cell frequently contains a flex row.
.gallery { display: grid; gap: 16px; /* as many columns as fit, each at least 240px */ grid-template-columns: repeat(auto-fill, minmax(240px, 1fr)); } /* an explicit page shape */ .page { display: grid; grid-template-columns: 240px 1fr; grid-template-areas: "side main"; }
The fr unit is a share of what is left after fixed sizes and gaps are taken out. minmax(240px, 1fr) with auto-fill is the single most useful line in modern CSS: a grid that reflows at every width without a single breakpoint.
auto-fill keeps empty tracks; auto-fit collapses them so the remaining items stretch. With one item, auto-fit makes it full width and auto-fill leaves it at 240px.
Responsive Design
Write the narrow layout first and add complexity as space appears. Starting wide means every breakpoint is an undo, and phones — the majority of traffic — get the version you tested least.
.layout { display: grid; gap: 16px; } /* one column by default */ /* add the sidebar only once there is room for it */ @media (min-width: 760px) { .layout { grid-template-columns: 240px 1fr; } } /* fluid type: scales, but never past sensible bounds */ h1 { font-size: clamp(1.6rem, 4vw, 3rem); }
Choose breakpoints where your layout breaks, not at device widths. Device lists go stale; the point at which your cards get too narrow does not.
Custom Properties & Theming
Custom properties are real CSS values, not build-time find-and-replace. They inherit, they can be changed at runtime, and they can be read and written from JavaScript — which is what makes theming with them straightforward.
:root { --bg: #0b1020; --text: #e7ecf5; --accent: #22d3ee; } /* one class on <html> swaps the whole palette */ html.light { --bg: #fffaf0; --text: #1a1a1a; --accent: #0e7490; } body { background: var(--bg); color: var(--text); }
Because they inherit, a custom property set on a component overrides the global one for that subtree only — which is how a single card gets its own accent without a new class for every colour.
Transitions & Animation
Animate transform and opacity. Those two can be handled by the compositor without recalculating layout or repainting. Animating width, height, top or margin forces layout on every frame, which is what janky animation actually is.
.card { transition: transform .18s ease, opacity .18s ease; } .card:hover { transform: translateY(-3px); } /* some people get motion sick. This is not optional. */ @media (prefers-reduced-motion: reduce) { * { transition-duration: .01ms !important; animation-duration: .01ms !important; } }
Keep interface motion short — 150 to 250ms. Anything longer stops reading as responsiveness and starts reading as lag.
Common Mistakes
| Mistake | What happens | Instead |
|---|---|---|
| Media query written above the rule it overrides | Silently loses the tie on source order | Put overrides last |
!important to win a fight | Only another !important can beat it | Fix the specificity |
Forgetting box-sizing: border-box | Padding pushes boxes past their width | Set it globally, once |
Animating width or top | Layout recalculated every frame | transform and opacity |
| Breakpoints copied from device sizes | Goes stale, misses your real break | Break where the layout does |
Dimming text with opacity to show state | Contrast quietly fails | Change the colour deliberately |
Interview Questions
Explain specificity.
Three counters: ids, then classes and pseudo-classes and attribute selectors, then elements. Compare left to right and the first difference wins. Ties go to whichever rule comes later. Inline styles outrank all selectors, and !important is a separate layer above everything.
Flexbox or Grid?
Flexbox for one direction — a toolbar, a row of chips. Grid for two — page structure, galleries. They combine: a Grid cell very often holds a flex row.
What does box-sizing: border-box change?
It makes width include padding and border rather than only the content box, so a 300px box stays 300px once you add padding. It is why almost every stylesheet opens with it.
Which properties are cheap to animate, and why?
transform and opacity. The compositor can handle both without recalculating layout or repainting. Animating width, height or top forces layout every frame.
What is margin collapsing?
Adjacent vertical margins merge into the larger of the two rather than adding up. It applies to block siblings in normal flow — not to padding, and not inside flex or grid containers, which is one reason gap is now preferred.
How do you build a grid that reflows without media queries?
grid-template-columns: repeat(auto-fill, minmax(240px, 1fr)). The browser fits as many 240px-minimum columns as it can and shares the remainder between them.
Quick Quiz
#hero p or .a.b.c.d p?justify-content in a flex row aligns items…minmax(240px, 1fr) with auto-fill gives you…