CSS — box model, flex, grid, specificity
📖 Walk me through it — plain English
CSS (Cascading Style Sheets) is the language that decides how things look and where they sit on a web page. HTML is the bare content (here is a heading, here is a button); CSS is the paint and the ruler. This lesson is a tour of the handful of CSS ideas that interviewers lean on when they say "center this box" or "build this layout from a screenshot." None of these are tricks — they are just the vocabulary you need so you spend the interview thinking about the problem, not fighting the page.
Start with the box model. Every element on a page is a rectangle made of layers, like a framed photo: the content (the photo) sits inside padding (the mat board around it), wrapped by a border (the frame). By default the width you set only measures the photo, so padding and border get added on top and the box ends up wider than you asked. Setting box-sizing: border-box flips that so width means the whole framed picture, edge to edge — much easier to reason about. The advice is to set it once for everything and never think about it again.
Flexbox and grid are the two tools for arranging boxes. Think of flexbox as a single shelf: items line up in one direction — a row or a column (that's what "1D", one-dimensional, means). justify-content slides them along that shelf (left, center, spread out); align-items nudges them across the shelf's depth. Grid is the whole bookcase: rows and columns at once (2D), which is why you reach for it for page-level structure and use flex for the smaller pieces inside. The "center a div" answer interviewers want is the flex one-liner: make the container display:flex, then align-items:center and justify-content:center, and the child lands dead center on both axes.
The trickiest idea is specificity — the rule that decides who wins when two CSS rules try to style the same thing differently. Browsers score each selector and the higher score wins. The ranking, weakest to strongest, is: a tag selector (like p) < a class (like .warning) < an id (like #nav) < an inline style="" written right on the element. Picture it as a leaderboard where the most specific selector takes the crown:
When two rules tie on score, the cascade breaks the tie: whichever one appears later in the source wins. That is why source order matters and why scoped styles or CSS modules (which keep each component's class names private) save you from accidental collisions. The escape hatch !important jumps the whole line — which is exactly why the lesson calls it "surrender": once you use it, the next person needs another !important to override you, and the leaderboard becomes meaningless. Prefer writing a more specific selector instead.
Last gotcha: z-index, which controls what stacks in front of what. The catch is that stacking is local — a child element can never pop in front of something outside its parent's "stacking context," no matter how huge its z-index. A stacking context is created quietly by things like position: relative, transform, or opacity below 1, so an element trapped inside one stays boxed in. When a layer "won't go on top no matter what number I give it," this trapped-in-a-parent situation is almost always why.
Frontend rounds love "center this div" and "build this layout from a screenshot." If flex/grid aren't reflexive, you waste interview minutes on layout instead of substance.
How CSS attaches to HTML
Before any of the layout ideas make sense, you need the one-sentence model of how CSS gets applied at all. A CSS rule has two halves: a selector (which elements does this target?) and a declaration block (a set of property: value; pairs to apply). The browser walks the page, matches selectors to elements, and paints the winning values. That is the whole game.
/* selector { property: value; property: value; } */
p {
color: steelblue; /* every <p> turns blue */
font-size: 16px;
}
A selector says what to style. The four you will use constantly: a type/tag selector (p, button) matches by element name; a class selector (.card, leading dot) matches any element with class="card" and is the workhorse you should reach for first; an id selector (#header, leading hash) matches the one element with that id; and a descendant selector (.card p, space between) matches a p anywhere inside a .card. Classes are reusable across many elements; ids are meant to be unique on the page.
The box model, with the math
Every element is a box drawn in four concentric layers, from the inside out: content (the text or image itself), padding (clear space inside the border, pushing the content away from the edge), border (the visible line around the padding), and margin (clear space outside the border that pushes other boxes away). A simple rule of thumb: padding is space on the inside, margin is space on the outside.
The classic gotcha is how width is measured. With the default box-sizing: content-box, the width you set applies only to the content; padding and border are then added on, so the box renders wider than the number you wrote:
/* default content-box: rendered width = content + padding + border */
.card {
box-sizing: content-box; /* the default */
width: 200px;
padding: 20px; /* 20 on each side = +40 */
border: 2px solid; /* 2 on each side = +4 */
}
/* actual painted width = 200 + 40 + 4 = 244px (surprise!) */
Switch to border-box and width means the whole box edge-to-edge (content + padding + border); the content area simply shrinks to make room. Now width: 200px really is 200px wide. Set it globally once and stop doing arithmetic in your head:
/* set once, near the top of your stylesheet, and forget it */
*, *::before, *::after {
box-sizing: border-box;
}
.card {
width: 200px;
padding: 20px;
border: 2px solid;
}
/* painted width = exactly 200px; content area = 200 - 40 - 4 = 156px */
Block vs inline
Elements come in two default flavors. A block element (div, p, h1, section) starts on a new line and stretches to fill the full width available — think of paragraphs stacking vertically. An inline element (span, a, strong) flows along inside a line of text and is only as wide as its content — like words in a sentence. Two consequences worth memorizing: inline elements ignore width, height, and top/bottom margins, and that catches people out. When you need an inline-ish thing that also honors box dimensions, use display: inline-block (flows inline, but accepts width/height). And display: none removes the box from the layout entirely (vs visibility: hidden, which hides it but keeps its space reserved).
Flexbox: the single shelf
Flexbox lays children out along one direction. You turn it on with display: flex on the container (the parent); its direct children become flex items. The direction is the main axis (a row by default, left-to-right); the perpendicular direction is the cross axis (top-to-bottom for a row). Two properties do most of the work: justify-content positions items along the main axis, and align-items positions them along the cross axis. Here is the canonical "center a div both ways" — the answer interviewers want first:
.parent {
display: flex;
justify-content: center; /* center on the main axis (horizontal) */
align-items: center; /* center on the cross axis (vertical) */
min-height: 100vh; /* give it height so there's room to center in */
}
/* the single child now sits dead-center, both axes */
A few more flex terms you will hear: flex-direction: column rotates the main axis to vertical (now justify-content controls vertical, align-items horizontal — they swap with the axis). gap sets consistent spacing between items and is cleaner than sprinkling margins. flex-wrap: wrap lets items spill onto a new line instead of overflowing. And the shorthand on each item, flex: 1, means "grow to share leftover space equally" — the standard way to make columns split the row evenly.
Grid: the whole bookcase
Grid lays children out in rows and columns at the same time (2D). You declare the column track sizes on the container and items flow into the cells. The handy unit here is fr (a "fraction" of the leftover space), and repeat(n, ...) saves typing:
.gallery {
display: grid;
grid-template-columns: repeat(3, 1fr); /* 3 equal columns */
gap: 16px; /* gutter between cells */
}
/* a responsive trick: columns that auto-fit and never get too narrow */
.gallery-fluid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 16px;
}
The interview heuristic: reach for grid when you are placing things in two dimensions (a page skeleton, an image gallery, a card grid), and reach for flex when you are arranging things along one line (a navbar, a row of buttons, a toolbar). They compose freely — a grid cell can itself be a flex container.
Position and z-index
The position property controls how an element is placed and what the top/right/bottom/left offsets mean. The five values:
static— the default; the element sits in normal document flow and ignorestop/leftoffsets.relative— stays in flow, but you can nudge it with offsets relative to where it would have been. Crucially, it also becomes the anchor for absolutely-positioned children.absolute— removed from flow and positioned relative to its nearest positioned ancestor (the closest parent that is itselfrelative/absolute/fixed/sticky). If none exists, it anchors to the page.fixed— removed from flow and pinned to the viewport; it stays put when you scroll (think sticky headers, chat bubbles).sticky— a hybrid: behaves likerelativeuntil you scroll past a threshold, then "sticks" likefixedwithin its container (section headers that cling to the top).
z-index decides which overlapping box paints on top — higher numbers win. The trap is that z-index only compares siblings within the same stacking context. A stacking context is an isolated layer-stack; an element trapped inside one can never paint above a sibling of its parent, no matter how large its z-index. Stacking contexts are created quietly by position with a z-index, but also by transform, opacity below 1, filter, and a few others. So "my z-index: 9999 still sits behind that thing" almost always means: look up the tree — an ancestor created a context with a lower stacking position, and your child is boxed inside it.
Units: px, em, rem, %
Sizes are written in units, and choosing the right one matters for accessibility and responsiveness:
px— an absolute pixel. Predictable and fixed; good for borders and fine details, but it does not scale when a user bumps their browser font size.rem— "root em": relative to the root font size (the<html>element, usually 16px).1.5rem= 24px by default. Because it tracks the user's base font setting,remis the preferred unit for font sizes and spacing in accessible designs.em— relative to the current element's font size. Handy for padding that should scale with the text inside a component, but it compounds when nested (aneminside anemmultiplies), which surprises people.%— relative to the parent's corresponding dimension (e.g.width: 50%= half the parent's width). Alsovw/vhexist for 1% of the viewport width/height.
Responsive design and media queries
Responsive means one layout that adapts to any screen width rather than separate desktop/mobile pages. The tool is the media query — a block of CSS that only applies when a condition (usually a width range) is met. The recommended approach is mobile-first: write your base styles for the small screen, then use min-width queries to add complexity as the screen grows. This keeps the simplest case as the default and layers on the rest:
/* base = mobile: one column, full width */
.layout {
display: grid;
grid-template-columns: 1fr;
}
/* once there's room, upgrade to two columns */
@media (min-width: 768px) {
.layout { grid-template-columns: 1fr 1fr; }
}
A breakpoint is just the width at which you change the layout (768px above). Pick breakpoints based on where your content starts to look cramped, not on specific device models. The modern successor, the container query (@container), reacts to the size of a component's own container instead of the whole viewport, which makes truly reusable components — worth naming if asked.
Common pitfalls
Adjacent vertical margins between block elements merge into the larger of the two instead of adding up — so a 20px bottom margin next to a 30px top margin yields a 30px gap, not 50px. It surprises everyone. Sidestep it by using gap in a flex/grid parent, or padding, instead of stacked margins.
Patching an override with !important forces the next person to escalate with their own !important, and soon nothing is predictable. Fix the root: add one more class to be slightly more specific, or scope styles (CSS modules) so collisions can't happen.
A giant z-index does nothing if the element is trapped in a parent's stacking context, or if the element isn't positioned (z-index needs a non-static position to take effect). Check both before raising the number again.
A box wider than you set almost always means default content-box sizing plus padding/border. Set box-sizing: border-box globally and the arithmetic disappears.
width = content by default. box-sizing: border-box makes width include padding+border. Set it once globally; never go back.
1D layout — row or column. justify-content on the main axis, align-items on the cross. gap > margins for spacing.
2D layout. grid-template-columns: repeat(3, 1fr). Use for page-level structure; flex for component-level.
display:flex; align-items:center; justify-content:center; — the canonical. Stop using position: absolute; transform: translate(-50%,-50%); in 2026.
inline > id > class > tag. !important = surrender. Prefer adding a more specific selector or restructuring CSS modules.
Later rules win at equal specificity. Source order matters. CSS modules / scoped styles solve most collision pain.
Stacking contexts are local. A child can't escape its parent's stacking context. position: relative creates one; so does transform, opacity < 1, etc.
Mobile-first. @media (min-width: ...). Don't hand-code pixel breakpoints all over — use container queries or a token set.
Takeaway: every element is a box (content / padding / border / margin) — use box-sizing: border-box so width behaves. Flex arranges along one axis (justify-content main, align-items cross); grid arranges in two. The flex-center trio is the canonical "center a div." Specificity is inline > id > class > tag, ties broken by source order — never paper over it with !important. And z-index only competes inside its own stacking context.
Go deeper (optional): MDN's "CSS layout" guide and the interactive references at css-tricks.com/snippets/css/a-guide-to-flexbox and /snippets/css/complete-guide-grid are the standard companions once you want every flex/grid property in one place.