gap·map

Flexbox & Grid

[ junior depth ]

How the main and cross axes work

Flexbox has no idea what "horizontal" means. It has a main axis, whatever flex-direction says, and a cross axis perpendicular to it. Every alignment property binds to an axis, not a direction: justify-content distributes along the main axis, align-items positions along the cross axis. Flip flex-direction: column and they swap jobs.

❌ "justify = horizontal, align = vertical" is the most common flexbox wrong answer. In a column, horizontal centering is align-items: center.

Three defaults do things people attribute to magic. flex-shrink: 1 means items silently compress when space runs out. align-items: stretch is where equal-height columns come from. And flex-wrap: nowrap means items shrink, then overflow; they never wrap unless asked.

flex: grow shrink basis

flex-basis is the starting size (auto means use width/height if set, otherwise content size). After that, free space gets distributed: surplus by flex-grow, deficit by flex-shrink. Learn the shorthand presets cold, because two of them look identical until content differs:

.a { flex: 1; }    /* 1 1 0    → basis 0: ALL space is shared → equal columns */
.b { flex: auto; } /* 1 1 auto → basis content: only LEFTOVER space is shared */

With basis 0, content size is ignored and items come out equal. With basis auto, an item with more text starts bigger and stays bigger. flex: none = 0 0 auto (rigid). Bare flex-grow: 1 leaves basis at auto, so it does not behave like flex: 1.

One more flex-only trick: margin: auto on an item absorbs free space. Put margin-left: auto on the last toolbar button and it moves to the far edge without any wrapper element.

How grid placement works

Grid works the opposite way from flexbox: you declare tracks, then place things into them.

.page {
  display: grid;
  grid-template-columns: 240px repeat(2, 1fr);
  gap: 16px;
}
.hero { grid-column: 1 / -1; } /* line 1 to last line: full bleed */

fr shares the space left over after fixed tracks and gaps, so fr columns can never overflow the container. ❌ "1fr is basically 33%" falls apart once you add gaps: percentages resolve against the whole container and happily overflow, while fr divides what remains. Placement uses line numbers, meaning the edges between tracks: 1 / 3 spans two tracks, and -1 is the last line.

grid-template-areas gives you the layout as ASCII art. Names must form rectangles, . means an empty cell, and one row with a different cell count invalidates the whole declaration.

When to use flexbox vs grid

Flexbox is content-out: a line of things negotiating for space (toolbar, tag list, nav). Grid is layout-in: a structure that exists before content, in two dimensions (page shell, card grid, form). If things need to align in both rows and columns, that is a grid job; one axis of items sharing space is flexbox. Neither replaces the other. A grid page full of flexbox components is the normal shape of a real app.

[ middle depth ]

The automatic minimum size

Every flexbox user eventually files this bug: a flex: 1 column blows past the viewport when a long URL or <pre> shows up. The mechanism sits in the defaults. In block layout min-width defaults to 0, but on a flex item it defaults to auto, roughly the item's min-content size. Shrinking is already enabled (flex-shrink: 1); it is forbidden from going below the longest unbreakable line.

.main { flex: 1; min-width: 0; }  /* releases the content floor */
.main pre { overflow-x: auto; }   /* NOW this can actually engage */

Any overflow other than visible on the item also resets the minimum. Grid has the identical trap wearing different syntax: 1fr expands to minmax(auto, 1fr), so a wide word holds its column open.

❌ "repeat(3, 1fr) guarantees equal columns". It does not; repeat(3, minmax(0, 1fr)) does.

justify-content vs align-items and the rest

Parse the property names instead of memorizing them. The prefix picks the axis: justify-* means the inline axis, align-* the block/cross axis. The suffix picks the target: -content moves the whole set of lines or tracks within the container, -items positions each thing inside its own cell or line, -self overrides one item. Grid has all six. Flexbox is missing justify-items and justify-self; on the main axis you only get distribution, and the escape hatch is margin: auto.

Two classics: justify-content in a column flex container does nothing without a height (no free space means nothing to distribute), and align-content needs multiple lines (flex-wrap: wrap) before it has anything to move.

The implicit grid

Items that don't fit the template don't overflow. They mint implicit tracks, auto-sized unless you say otherwise:

.gallery {
  grid-template-columns: repeat(4, 1fr);
  grid-auto-rows: minmax(120px, auto); /* size the rows you didn't declare */
  grid-auto-flow: dense;               /* backfill holes left by spanning items */
}

dense packs tightly and decouples visual order from DOM order, while tab and screen-reader order stay with the DOM. That makes it (like order) an accessibility decision, not a styling one.

auto-fill vs auto-fit

Both create as many tracks as fit the container. They differ only when tracks end up empty: auto-fill keeps them (real items stay at track width), auto-fit collapses them to zero so the survivors' 1fr max absorbs the row. With enough items they render identically, which is why the difference ambushes people in the "2 cards on a wide screen" state.

grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));

That one line is a responsive card grid with zero media queries.

Container queries vs media queries

Media queries answer questions only the viewport knows: (width >= 40rem) (use range syntax), (hover: hover), (prefers-reduced-motion: reduce), (prefers-color-scheme: dark). But a card in a 300px sidebar and the same card in a 900px column share one viewport, and container queries exist for that case:

.sidebar, .main-col { container-type: inline-size; }
@container (width > 400px) { .card { grid-template-columns: 120px 1fr; } }

The query targets the nearest ancestor container. It can style the container's descendants but never the container itself (that would be circular, since the styles could change the size being queried). cqi and cqw units size things relative to the container the way vw does for the viewport.

[ senior depth ]

min-content, max-content, and the flex algorithm

Under both layouts sits one vocabulary: min-content (the largest unbreakable chunk), max-content (never wrap at all), fit-content(n) (max-content, clamped to n, floored at min-content). Flex resolution in one breath: each item starts at its basis; positive free space is handed out weighted by flex-grow; negative space is reclaimed weighted by flex-shrink × basis (bigger items give back more); items that hit a min or max clamp are frozen and the remainder is redistributed. Every "weird" flex width is one of those clamps firing, usually the automatic minimum. Once you can narrate that loop, the difference between flex: 1, flex: auto, and flex-grow: 1 is arithmetic rather than trivia.

Grid asks for the same literacy: track lists like fit-content(30ch) and minmax(min-content, 1fr) are legitimate answers, and 1fr quietly expanding to minmax(auto, 1fr) reproduces the min-width: auto behavior in grid.

How subgrid aligns content across cards

Nested grids create independent tracks, so card internals never line up across cards. Fixed heights and JS measurement are the historical hacks; subgrid replaces both:

.cards { display: grid; grid-template-columns: repeat(3, 1fr); }
.card  { grid-row: span 3; display: grid; grid-template-rows: subgrid; }
/* title / body / footer rows now shared by every card */

The subgridded axis adopts the parent's tracks, gaps (overridable), and line names. Two sharp edges: line numbers restart inside the subgrid, and a subgridded axis cannot create implicit tracks. Overflow items have nowhere to go, so subgrid one axis and keep grid-auto-rows on the other if the item count varies.

Responsive architecture without breakpoints

Reach for a breakpoint only after the layout has failed to size itself. The ordering: intrinsic sizing first, meaning repeat(auto-fit, minmax(250px, 1fr)), flex-wrap plus flex-basis as a built-in "too narrow, wrap" switch, and clamp() for fluid type. Container queries second, as the component's own contract: it adapts to its slot and stays correct when a redesign moves it. Media queries last, reserved for what only the viewport knows: input capability (hover, pointer), user preferences (prefers-reduced-motion, prefers-color-scheme), and global shell decisions. A component that reads the viewport to guess its own width is coupling to a coincidence.

The dependency underneath: container-type works because it applies containment. The browser can only promise a stable answer to "how wide is my container" if the container's size can't depend on the contents being styled, which is also why a query can't style its own container.

Flexbox and grid performance

Flex and grid layout are fast; invalidation is what bites. Deeply nested flex chains can force multi-pass measurement (the parent asks for the child's size while the child's size depends on the parent), so flatten where cheap. Never animate grid-template-columns or flex-basis in hot paths: that costs a layout pass every frame. Animate transform and fake the rest (gap is the tolerated exception, and track animation support is still patchy anyway). For thousand-card grids, content-visibility: auto skips off-screen layout entirely.

Interviewers at this band probe judgment calls more than syntax: dense and order are accessibility tradeoffs you document, not styling choices; grid-template-areas earns its keep on stable shells and fights you on dynamic children; and the grid shorthand silently resets grid-auto-*, so prefer the longhands in shared code.

beta

The interviewer part is in the works.

The diagnostic, personal maps, and AI mock interviews are being finished right now. The notes stay free either way. Leave an email and you'll get the first-cohort invite, plus a month of Pro when it opens.

builds on

more CSS