Skip to content

CSS Flexbox, Grid & Responsive

Key Points

  • box-sizing: border-box on * 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-width media 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:

*, *::before, *::after { box-sizing: 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

body { min-height: 100vh; display: flex; flex-direction: column; }
main { flex: 1; }

Holy grail (header, footer, three columns) — Grid is honestly easier; flex needs nesting.

Equal columns

.cols { display: flex; gap: 1rem; }
.cols > * { flex: 1; }

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)

.cards {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
  gap: 1rem;
}

"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

.video-wrap { aspect-ratio: 16 / 9; }     /* replaces the padding-bottom hack */

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

.col { float: left; width: 33.33%; }

It's 2026. Don't.

✅ Correct: grid

.cols { display: grid; grid-template-columns: repeat(3, 1fr); gap: 1rem; }

❌ Wrong: margin for inter-item spacing in flex/grid

.row > * + * { margin-left: 1rem; }

Breaks on wrap; no symmetry.

✅ Correct: gap

.row { display: flex; gap: 1rem; flex-wrap: wrap; }

❌ Wrong: viewport-only responsive

@media (min-width: 768px) { .card { display: grid; } }

Card behaves identically in a 200px sidebar and a 1000px main column.

✅ Correct: container queries

.host { container-type: inline-size; }
@container (min-width: 400px) { .card { display: grid; } }

❌ 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

h1 { font-size: clamp(2rem, 1.5rem + 2.5vw, 3.5rem); }

Design patterns for this topic

Pattern 1 — "Auto-fit responsive grid"

  • Intent: card walls without media queries.
grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));

Pattern 2 — "Token-driven theming"

  • Intent: swap themes by changing one set of variables.
:root { --bg: white; --fg: black; }
[data-theme="dark"] { --bg: #111; --fg: #eee; }

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-start flips 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.
  • ⚠️ 100vh on mobile — includes the URL bar's space; use 100dvh (dynamic viewport).
  • ⚠️ z-index only works on positioned elements — needs position: relative/absolute/fixed/sticky.
  • ⚠️ Specificity wars — reach for @layer, not !important.
  • ⚠️ Container without container-type@container does nothing without it.
  • ⚠️ min-content / max-content confusionmin-content shrinks to longest unbreakable token; max-content expands fully.
  • ⚠️ Mixing gap with old browsersgap works in flex from 2021+. Old IE/Safari issues are mostly past, but check telemetry.

Further reading