The layout question in modern CSS is usually asked as a war, grid versus flexbox, and the war is as unnecessary as it is persistent. The two systems were designed for different dimensional problems, grid for the two dimensional arrangement where rows and columns exist as a design, flexbox for the one dimensional flow where a group of items shares a line and a distribution. The mature layout practice does not choose between them, it composes them, page scaffolding in grid, the component internals in flexbox, and the moments where one tool does the other's job are the exceptions the practice recognizes rather than the proof one tool should win. This article draws the dimensional line, walks real layout compositions from a production dashboard, covers the alignment and gap vocabulary that replaced the hack era, and ends with the working rules that keep the choice mechanical instead of philosophical.
The dimensional intent that separates the tools
Flexbox lays out content along a single axis. The container's children flow, horizontally or vertically, they grow and shrink against each other by ratio, and the line wraps when the space runs out, with each new line an independent flex line. The model's genius is distribution, the justify-content spacing, the flex-grow ratios, the perfect vertical centering that took a decade of hacks before it:
.toolbar {
display: flex;
justify-content: space-between;
align-items: center;
gap: 12px;
}
The toolbar does not know how many buttons it holds, it does not care, and the layout is a property of the group rather than a map of positions.
Grid lays out structure in two dimensions at once. The container declares tracks, rows and columns, the items are placed into cells, and the relationship between rows and columns is the design:
.page {
display: grid;
grid-template-columns: 240px 1fr;
grid-template-rows: 56px 1fr;
grid-template-areas:
"header header"
"sidebar main";
gap: 24px;
}
The page knows exactly what it is, a header across the top, a sidebar beside the content, and the template areas read as the drawing the designer drew. The choice between the tools is the choice between these two statements, a group of items sharing a line, or a structure holding regions, and almost every layout question in a real application is answered by reading which of the two sentences describes it.
The page scaffolding that grid does best
The application shell is grid's signature piece, and the named areas make it self documenting:
.header { grid-area: header }
.sidebar { grid-area: sidebar }
.main { grid-area: main }
The responsive behavior is a template change rather than a component change, the same regions, rearranged by one media query that swaps the area map:
@media (max-width: 720px) {
.page {
grid-template-columns: 1fr;
grid-template-areas: "header" "main" "sidebar";
}
}
The components inside did not move, did not restructure, did not learn about the breakpoint, and the layout change is a property of the scaffold, one place, one intent. The card grids and the content listings take the same shape with a different emphasis, auto-fill tracks with a minmax floor, the responsive without the query:
.cards {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(240px, 1fr));
gap: 16px;
}
The minmax is the honest boundary of the pattern, a floor that prevents the squeezed card and a ceiling that lets the row breathe, and the auto-fill wraps at whatever width the container actually has, which composes with the container queries that modern browsers ship and the nested grids that card layouts want.
The component internals that flexbox does best
Inside every region of the scaffold, the components are groups, and flexbox serves the group. The card with an icon, a title, a description and an action row:
.card {
display: flex;
flex-direction: column;
gap: 8px;
}
.card__title {
display: flex;
align-items: center;
gap: 8px;
}
The title row centers the icon with the text without a calculation, the description grows to fill and push the action row to the bottom, flex-grow on the middle element, and the card's internals never needed to know the card's width. The navigation, the button with icon and label, the form row with the label, the input and the hint, the stat block with the value and the trend arrow, every one of them is a line with a distribution, and the flex vocabulary, gap, justify, align, grow, shrink, is the complete toolkit for all of them.
The place flexbox still surprises is the wrap behavior, and the surprise is worth the sentence. A wrapped flex container distributes within each line, not across lines, so the stretched last row of four items where three wrapped, three items stretching to the row's width, is the model working as specified. The gap property ended the era of margin hacks in both directions, and the alignment shifted baseline, align-items with the baseline option, covers the mixed content rows, the label beside the input, that the hack era solved with vertical-align folklore.
Container queries and the component sized layout
The responsive rules historically belonged to the page, the media query watched the viewport and every component either knew its breakpoints or pretended not to, and the component that moved between the sidebar and the full width main region rendered badly in one of its homes. The container query closes that gap, and it composes with this article's two tools rather than replacing either:
.card-container { container-type: inline-size }
@container (min-width: 400px) {
.card { grid-template-columns: 96px 1fr }
}
The card stops asking how wide the window is and asks how wide its own container is, and the same card renders the compact stack in the sidebar and the horizontal layout in the main region without the scaffold knowing either existed. The composition completes the practice, the scaffold is grid with its area templates, the components are flex and grid inside, and the component's own responsive rules follow the container, which is the only width the component was ever really sensitive to. The teams adopting container queries report the same cleanup, the shared breakpoint constants disappear from the component layer, and the layout vocabulary from this article becomes the component's entire relationship with the screen.
One dashboard composed end to end
The composition reads best as one artifact. The analytics dashboard, the standard product shape, is one grid scaffold, three regions:
.dashboard {
display: grid;
grid-template-columns: 280px 1fr;
grid-template-rows: 48px 1fr;
grid-template-areas: "topbar topbar" "nav content";
gap: 16px;
height: 100vh;
}
The topbar is flex, brand left, actions right, one line of intent. The nav is a flex column with gap, the items grow a hover region and the active state is a two column micro-grid inside each item, icon and label. The content region is a grid of cards, the auto-fill minmax pattern from earlier, and each card is a flex column whose header row is flex and whose chart area is a single grid cell holding the visualization. Every layout decision in the dashboard is one of the two tools used at its dimensional strength, the whole file contains no absolute positions, no floats, and no margin arithmetic, and a new joiner reads the scaffold in ten seconds and the components in one pass each.
The alignment and gap vocabulary that replaced the hacks
Both systems share the alignment properties, and the shared vocabulary is why the pair feels like one layout language. justify-content distributes along the main axis, the flex direction or the inline axis of grid, align-content distributes the rows of a grid or the lines of a wrapped flex, align-items aligns children within their track or line, and the place shorthand sets both. The gap property, once a grid exclusive and now in both, replaced the margin based spacing arithmetic that the flexbox era never made honest, the last child exception, the half gap on the parent, the negative margin wrapper, and the container that simply states its internal spacing is the improvement that every team notices within a week of the upgrade.
The auto margins remain the precision instrument inside both systems, margin-left: auto pushing an item to the far end of the line, the pattern that flexbox's own documentation recommends for the one item that goes to the right when the rest stay left, and the single line of intent beats a three property arrangement of justify and order every time it applies.
Subgrid and the alignment that crosses components
The one structural gap in the composition was the card that needed its internals to line up with its siblings' internals, the product listing where every card's title must align with every other card's title regardless of description length. The era before subgrid solved it with equal heights and internal flex grow, which aligned approximately, and the subgrid value closes it exactly, the child grid adopting the parent's tracks:
.cards { display: grid; grid-template-columns: repeat(3, 1fr) }
.card { display: grid; grid-template-rows: subgrid;
grid-row: span 3 }
Now the three rows of every card, image, title, actions, are the same three rows across the whole listing, the titles align at the line the tracks define, and the mechanism is the scaffold's geometry crossing the component boundary in the direction designers always drew it. The support is current in the evergreen browsers, the pattern degrades to per card layout without it, and the listings and comparison tables that adopt it retire the last of the equal height folklore.
Debugging layouts in the devtools
The layout debugging workflow is the quiet dividend of the two tool practice, because the browser's layout pane renders both models visually. The grid overlays draw the track lines and the area names, the flex overlays draw the distribution axes and the gaps, and the one click that shows why an item landed where it did replaces the computed style archaeology of the float era. The practice worth adopting in review is the overlay reproduction, when a layout defect appears, the inspector screenshot with the overlay on is attached to the ticket, and the fix review starts from the picture of the geometry rather than a paragraph of prose about it.
The overlaps and the working rules for choosing
The overlaps are real and worth naming so they are decisions rather than confusions. A single row of equal boxes, the avatar list, is both tools' territory, flex with wrap or grid with auto-fill, and the tiebreaker is the wrap semantics, flex wraps into independent lines and grid keeps the two dimensional rhythm, the product wants the rhythm when the boxes are a gallery and wants the flow when the boxes are avatars. A centered single item is both tools' one liner, and either answer is correct enough that the team should stop asking. A complex component with internal structure wants grid inside, a media object with fixed and flexible parts, and the scaffold around it stays grid, and the nesting is the composition the practice is named for.
The rules that make the choice mechanical fit in one list:
- Ask whether the layout is a structure of regions or a group of items, grid for the first, flexbox for the second;
- Scaffold the page and the major sections with grid template areas, and read the drawing back from the code;
- Build component internals with flex and gap, centering and distribution as one line each;
- Use minmax and auto-fill for the responsive card patterns before reaching for media queries;
- Nest freely, the scaffold is grid, the card is flex, the card's thumbnail row is grid again;
- Reserve the philosophical debates for the one overlap case per year that genuinely deserves one.
The two systems are one layout language with two dimensional moods, and the codebase that uses each where its model fits reads like the designer's drawing translated, regions where the design drew regions, flow where the design drew a group, and no hack vocabulary left in either place. The dogma was always the wrong frame, the composition was always the practice, and the modern CSS that ships both systems side by side was built for exactly that.