Frontend · Guide

CSS

Layout and presentation: the cascade, the box model, Flexbox and Grid, responsive design, custom properties and motion.

— min read Frontend

How CSS Decides

Most CSS frustration is not about properties. It is about not knowing which rule won, and why.

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.

SelectorSpecificityReads as
p0-0-1One element
.card0-1-0One class
.card p0-1-1Class + element
.card.dark p0-2-1Two classes + element
#hero1-0-0Beats every combination of classes
style="…"inlineBeats 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.

CSS — the one line nearly every project starts with
/* 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.

positionPositioned relative toLeaves a gap behind?
staticNormal flow (the default)
relativeWhere it would have beenYes, space is kept
absoluteNearest positioned ancestorNo, removed from flow
fixedThe viewportNo
stickyFlow until a scroll thresholdYes
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.

CSS — the flex properties you will actually use
.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.

CSS — a responsive gallery with no media query
.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.

CSS — mobile first, and breakpoints on content
.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.

Test at 320px. It is still the narrowest screen in common use, and it is where fixed widths and long unbroken strings cause the horizontal scrolling that makes a page feel broken.

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.

CSS — a two-theme setup in a dozen lines
: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.

When you swap a palette, re-check contrast. A colour that reads clearly on a dark background is often unreadable on a light one, and the failure is invisible in the theme you happened to be looking at.

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.

CSS — cheap motion, and respecting the setting
.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

MistakeWhat happensInstead
Media query written above the rule it overridesSilently loses the tie on source orderPut overrides last
!important to win a fightOnly another !important can beat itFix the specificity
Forgetting box-sizing: border-boxPadding pushes boxes past their widthSet it globally, once
Animating width or topLayout recalculated every frametransform and opacity
Breakpoints copied from device sizesGoes stale, misses your real breakBreak where the layout does
Dimming text with opacity to show stateContrast quietly failsChange 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

1. Which wins: #hero p or .a.b.c.d p?
2. Two rules of equal specificity both match. Which applies?
3. justify-content in a flex row aligns items…
4. Which pair is cheapest to animate?
5. minmax(240px, 1fr) with auto-fill gives you…