CSS Flexbox, Grid & Responsive
Key Points
box-sizing: border-boxon*is the modern default. Width includes padding + border. Set it once, forget it.- Flexbox is 1-D (a row or a column). Grid is 2-D (rows AND columns simultaneously). Pick by dimensionality, not by familiarity.
- Mobile-first: write base styles for the smallest screen, layer on
min-widthmedia queries upward. Cheaper to add than to undo. - Container queries (
@container) let a component respond to its parent's size, not the viewport. Long-overdue fix for component reusability. - CSS custom properties (variables) cascade and are runtime-mutable from JS — that's their whole point vs. preprocessor variables.
clamp(min, ideal, max)for fluid typography and spacing. Replaces 80% of the breakpoint-soup font-size adjustments.
Concepts (deep dive)
The box model
┌──────────────── margin ────────────────┐
│ ┌──────────── border ────────────┐ │
│ │ ┌────────── padding ──────┐ │ │
│ │ │ content (W×H) │ │ │
│ │ └─────────────────────────┘ │ │
│ └────────────────────────────────┘ │
└────────────────────────────────────────┘
Default box-sizing: content-box measures content only — padding and border push the box outward. box-sizing: border-box measures the visible box — padding and border eat into the content area. Always border-box:
Flexbox
One axis at a time: a main axis and a cross axis.
flex-direction: row flex-direction: column
main → main
┌──┬──┬──┐ ↓
│ │ │ │ cross ↓ ┌──┐
└──┴──┴──┘ │ │
├──┤
│ │
├──┤
│ │
└──┘ cross →
| Property | Effect |
|---|---|
display: flex | Becomes a flex container |
flex-direction | row (default) / column / *-reverse |
justify-content | Main-axis alignment |
align-items | Cross-axis alignment of items |
align-content | Cross-axis alignment of lines (when wrapping) |
flex-wrap | nowrap (default) / wrap |
gap | Spacing between items (replaces margin hacks) |
flex: g s b | shorthand for flex-grow flex-shrink flex-basis |
.row {
display: flex;
gap: 1rem;
align-items: center;
justify-content: space-between;
}
.spacer { flex: 1; } /* fill remaining space */
.fixed { flex: 0 0 200px; } /* exactly 200px, no grow/shrink */
Common flex patterns
Sticky footer
Holy grail (header, footer, three columns) — Grid is honestly easier; flex needs nesting.
Equal columns
Grid
Two axes at once. Define columns and rows; place items.
.grid {
display: grid;
grid-template-columns: repeat(3, 1fr); /* 3 equal columns */
grid-template-rows: auto 1fr auto; /* header / body / footer */
gap: 1rem;
}
fr is the "fractional unit" — distributes leftover space proportionally. 1fr 2fr = 1:2 split.
repeat + minmax + auto-fit (the responsive cheat code)
"As many 240px-min columns as fit; expand the rest." No media queries. Works.
Named areas
.app {
display: grid;
grid-template-columns: 200px 1fr;
grid-template-rows: 60px 1fr 40px;
grid-template-areas:
"header header"
"sidebar main"
"footer footer";
min-height: 100vh;
}
.app > header { grid-area: header; }
.app > nav { grid-area: sidebar; }
.app > main { grid-area: main; }
.app > footer { grid-area: footer; }
Subgrid
A child grid inheriting tracks from its parent — eliminates the "nested cards don't align" bug:
.outer { display: grid; grid-template-columns: 1fr 1fr 1fr; }
.inner { display: grid; grid-column: span 3; grid-template-columns: subgrid; }
Flex vs Grid — pick by dimensionality
| You have | Use |
|---|---|
| A row of buttons | Flex |
| A column of stacked cards | Flex |
| A page layout (header/sidebar/main/footer) | Grid |
| A photo wall that reflows | Grid + auto-fit minmax |
| A toolbar with grow/shrink elements | Flex |
| A dashboard of tiles aligned in both axes | Grid |
Rule of thumb: if you find yourself nesting flexes 3 levels deep to align rows and columns, you wanted grid.
Responsive design
Mobile-first breakpoints
.card { padding: 1rem; } /* base = mobile */
@media (min-width: 768px) { .card { padding: 1.5rem; } } /* tablet */
@media (min-width: 1200px) { .card { padding: 2rem; } } /* desktop */
Common breakpoint scale: 640 / 768 / 1024 / 1280 / 1536. Tailwind's defaults are reasonable; pick once and stick.
Container queries
Component-driven, not viewport-driven:
.card-host { container-type: inline-size; container-name: card; }
@container card (min-width: 400px) {
.card { display: grid; grid-template-columns: 120px 1fr; }
}
The same .card adapts to whatever container it's dropped in — sidebar, main, modal. This is what made truly reusable components possible in plain CSS.
Fluid typography with clamp
:root {
--fs-body: clamp(1rem, 0.95rem + 0.25vw, 1.125rem);
--fs-h1: clamp(2rem, 1.5rem + 2.5vw, 3.5rem);
}
body { font-size: var(--fs-body); }
h1 { font-size: var(--fs-h1); }
clamp(min, preferred, max) — scales between bounds based on viewport. No media queries needed.
aspect-ratio
CSS custom properties (variables)
:root {
--brand: #0066cc;
--gap: 1rem;
--radius: 8px;
}
.btn {
background: var(--brand);
border-radius: var(--radius);
}
[data-theme="dark"] {
--brand: #4da3ff; /* same selector tree, different value */
}
Live at runtime — JS can mutate them: el.style.setProperty("--brand", "red"). Cascade like any property. Not the same as Sass variables (those are compile-time).
Modern CSS worth knowing
| Feature | What it does |
|---|---|
:has() | "Parent selector" — .card:has(img) matches cards containing an image |
@layer | Cascade layers — explicit precedence groups, beats specificity wars |
color-mix() | color-mix(in srgb, var(--brand) 30%, white) — runtime color blend |
| Logical properties | margin-inline-start instead of margin-left — RTL-safe |
scroll-snap-* | Native carousel-style snap points |
view-transition | Animate between DOM states across navigations |
:focus-visible | Focus ring only when keyboard-focused, not mouse-clicked |
:where() / :is() | Group selectors; :where has zero specificity |
accent-color | Native form-control tinting |
/* Cascade layers — explicit precedence */
@layer reset, base, components, utilities;
@layer components { .btn { padding: 0.5rem 1rem; } }
@layer utilities { .p-0 { padding: 0; } }
/* utilities always wins, regardless of selector specificity */
Approaches: vanilla, CSS-in-JS, utility-first
| Approach | Examples | Best for |
|---|---|---|
| Vanilla / BEM | Plain .css files, BEM naming | Server-rendered apps (Razor, MVC), simple SPAs |
| CSS Modules | .module.css scoped per file | React/Angular component isolation |
| CSS-in-JS | styled-components, Emotion | Heavy theming, dynamic styles tied to props (perf cost — runtime) |
| Utility-first | Tailwind, UnoCSS | Speed of iteration; design-system enforcement |
| Zero-runtime CSS-in-JS | Vanilla Extract, Linaria | Type-safe styling, no runtime cost |
Tailwind is the dominant utility-first system. Trade-off: HTML gets dense (class="px-4 py-2 rounded-md bg-blue-600 hover:bg-blue-700 text-white"), but the design system is enforced and dead code is purged at build.
Code: correct vs wrong
❌ Wrong: float-based layout
It's 2026. Don't.
✅ Correct: grid
❌ Wrong: margin for inter-item spacing in flex/grid
Breaks on wrap; no symmetry.
✅ Correct: gap
❌ Wrong: viewport-only responsive
Card behaves identically in a 200px sidebar and a 1000px main column.
✅ Correct: container queries
❌ Wrong: fixed font-size scale + breakpoints
h1 { font-size: 32px; }
@media (min-width: 768px) { h1 { font-size: 40px; } }
@media (min-width: 1200px) { h1 { font-size: 56px; } }
✅ Correct: clamp
Design patterns for this topic
Pattern 1 — "Auto-fit responsive grid"
- Intent: card walls without media queries.
Pattern 2 — "Token-driven theming"
- Intent: swap themes by changing one set of variables.
Pattern 3 — "Container-query components"
- Intent: components react to their slot's size, not the viewport.
Pattern 4 — "Cascade layers for design system"
- Intent: lock predictable precedence — reset < base < components < utilities.
Pattern 5 — "Logical properties for i18n"
- Intent:
margin-inline-startflips automatically in RTL languages.
Pros & cons / trade-offs
| Aspect | Pros | Cons |
|---|---|---|
| Flexbox | Simple, 1-D, broad support | Awkward for 2-D layouts |
| Grid | True 2-D, named areas | Slight learning curve |
| Container queries | Component-driven | Newer (≥ Chrome 105 / Safari 16) |
| Utility-first (Tailwind) | Velocity, design enforcement | Verbose markup |
| CSS-in-JS (runtime) | Dynamic theming | Runtime cost; SSR complexity |
| Logical properties | RTL out of the box | Two property names to know |
When to use / when to avoid
- Use Grid for page-level and 2-D layouts.
- Use Flex for toolbars, button rows, single-axis distribution.
- Use container queries for any component meant to land in multiple slots.
- Avoid runtime CSS-in-JS in performance-critical SSR apps.
- Avoid floats,
clearfix, table layouts. They are historical.
Interview Q&A
Q1. Flex vs Grid — pick which when? Flex for 1-D (row OR column). Grid for 2-D and page layouts. If you nest flex containers to align two axes, you wanted grid.
Q2. What does box-sizing: border-box do? Width/height include padding and border. Without it, padding pushes the box bigger than its declared width.
Q3. What's 1fr? A "fractional" share of remaining grid track space. 1fr 2fr distributes leftover space 1:2.
Q4. repeat(auto-fit, minmax(240px, 1fr)) — what happens? As many columns as fit at ≥240px width; remaining space is split equally. Responsive grid without media queries.
Q5. Container queries vs media queries? Media: viewport-relative. Container: parent-relative — same component adapts to whichever slot it's in.
Q6. Mobile-first vs desktop-first? Mobile-first uses min-width breakpoints layered upward. Cheaper to add styles than to undo them. Industry default.
Q7. CSS custom properties vs Sass variables? Custom properties live at runtime, cascade, are JS-mutable. Sass variables are compile-time only.
Q8. What does :has() enable? Parent selection: .row:has(.error) styles a row containing an error. Long-missing capability.
Q9. What is clamp() good for? Fluid values with a floor and ceiling — typography, spacing, container widths. Replaces breakpoint soup.
Q10. Cascade layers? @layer declares explicit precedence buckets. Higher layer always wins, regardless of selector specificity.
Q11. Logical vs physical properties? Logical properties (margin-inline-start) follow writing direction, flipping correctly for RTL. Physical (margin-left) don't.
Q12. Tailwind trade-offs? Pro: enforced design system, fast iteration, dead-code purge. Con: dense markup, learning the utility names.
Gotchas / common mistakes
- ⚠️ Forgetting
box-sizing: border-box— every measurement is off. - ⚠️ Margin collapse — vertical margins between siblings/parents merge. Use padding or flex/grid
gap. - ⚠️
100vhon mobile — includes the URL bar's space; use100dvh(dynamic viewport). - ⚠️
z-indexonly works on positioned elements — needsposition: relative/absolute/fixed/sticky. - ⚠️ Specificity wars — reach for
@layer, not!important. - ⚠️ Container without
container-type—@containerdoes nothing without it. - ⚠️
min-content/max-contentconfusion —min-contentshrinks to longest unbreakable token;max-contentexpands fully. - ⚠️ Mixing
gapwith old browsers —gapworks in flex from 2021+. Old IE/Safari issues are mostly past, but check telemetry.