const lab = document.querySelector('[data-layout-lab]');
const form = lab.querySelector('form');
const stage = lab.querySelector('[data-stage]');
const widthLabel = lab.querySelector('[data-width-label]');
const titles = Array.from(lab.querySelectorAll('[data-card-title]'));
const verdict = lab.querySelector('[data-verdict]');
const reason = lab.querySelector('[data-reason]');
const checks = lab.querySelector('[data-checks]');
const cardTitles = {
normal: ['Checkout summary', 'Refund states', 'German labels'],
dense: ['Checkout summary with applied promotions', 'Refund states across payment providers', 'German labels in account settings'],
long: [
'Checkout summary with unavailable payment methods and stacked promotion rules',
'Refund states across payment providers, manual review, and failed captures',
'German labels for account-level notification preferences and billing history',
],
};
const notes = {
flex: {
verdict: 'Flexbox is useful only while the job stays one-axis.',
reason: 'Wrapping distributes cards into available space, but row alignment becomes a side effect once content lengths diverge.',
checks: [
'Good for a toolbar or a loose list of chips.',
'Risky when cards need comparable columns, metadata, or action placement.',
],
},
grid: {
verdict: 'Grid is the strongest default here.',
reason: 'Cards need predictable columns and aligned actions, so the two-axis relationship matters more than simple distribution.',
checks: [
'Tracks absorb unequal text without changing the component contract.',
'Actions remain visually comparable across rows.',
],
},
query: {
verdict: 'Container queries make the component own the breakpoint.',
reason: 'The preview changes at the card rail width, not at the viewport, which is the safer rule for reusable components.',
checks: [
'Use this when the same component appears in a sidebar, modal, and wide page slot.',
'Keep page-level media queries for the surrounding shell.',
],
},
};
function renderList(items) {
checks.replaceChildren();
items.forEach((item) => {
const li = document.createElement('li');
li.textContent = item;
checks.append(li);
});
}
function render() {
const values = new FormData(form);
const mode = values.get('mode') || 'grid';
const density = values.get('density') || 'normal';
const width = Number(values.get('width') || 760);
const activeNotes = notes[mode];
lab.dataset.mode = mode;
lab.dataset.density = density;
stage.style.setProperty('--stage-width', width + 'px');
widthLabel.textContent = width + 'px';
cardTitles[density].forEach((title, index) => {
titles[index].textContent = title;
});
verdict.textContent = activeNotes.verdict;
reason.textContent = activeNotes.reason;
const extraCheck = width < 520 && density !== 'normal'
? 'At this width, the layout also needs overflow-wrap, stable actions, and a clear wrapping policy.'
: 'The layout decision should still be tested with the longest expected content.';
renderList(activeNotes.checks.concat(extraCheck));
}
form.addEventListener('input', render);
render();
Most teams do not have a Grid problem or a Flexbox problem. They have a layout relationship problem. The fastest way to make the right choice is to name the relationship before writing the declaration.
If rows and columns both matter, start with Grid. If the layout distributes items along one axis, start with Flexbox. If a child layout must align to tracks owned by a parent, consider Subgrid. If the component needs to change based on its allocated space instead of the viewport, add container queries.
The decision test
Ask what would make the layout wrong if the content changed.
If cards must line up in both rows and columns, you need Grid.
If buttons should wrap and keep natural widths, Flexbox is usually enough.
If a nested heading and body text must align with sibling cards, Subgrid can remove duplicate track math.
If the same component appears in a sidebar and a main column, viewport media queries may be the wrong boundary.
This matters because layout bugs often come from solving the wrong relationship. A toolbar written with Grid can become rigid. A dashboard written only with Flexbox can lose column alignment. A reusable card tuned with viewport breakpoints can be wrong in every container except the one where it was designed.
Grid is for shared structure
Grid is strongest when the parent owns a two-dimensional structure. You can name areas, size tracks, align items, and let auto-placement fill predictable slots.
The minmax(0, 1fr) detail is not decoration. It tells the flexible track that it is allowed to shrink below the min-content size of its children. Without it, a long table, code sample, or unbroken URL can push the layout wider than the viewport.
Flexbox is for distribution
Flexbox is strongest when items need to size from their content and distribute remaining space along one axis.
This works because the toolbar is not asking items to share vertical tracks. It is asking them to sit in a row, wrap when needed, and preserve natural button sizes. The spacer pushes secondary actions away without inventing extra columns.
The card uses Grid because its vertical regions matter: heading, body, actions. The metadata uses Flexbox because it is a one-axis cluster. That separation keeps the component readable: Grid owns structure, Flexbox owns distribution.
Do not ask one layout mode to do every job. A Grid toolbar can work, but every new action may require a track decision. A Flexbox dashboard can work, but every aligned region becomes a negotiation between wrapped lines. If the CSS reads like a workaround, the layout relationship may be assigned to the wrong primitive.
The production rule
Choose the primitive that describes the invariant. If the invariant is “these columns align,” use Grid. If the invariant is “these controls flow together,” use Flexbox. If the invariant is “this nested content inherits parent tracks,” use Subgrid. If the invariant is “this component responds to its own width,” use a container query.
Keep source order meaningful before layout enters the conversation. Grid and Flexbox can both make visual order diverge from document order, but that power should not be used to paper over bad markup.
Debug by changing the content
When the choice is unclear, stress the layout. Add a long heading, remove an image, add one more action, translate a label, and narrow the container. The primitive that still describes the relationship is usually the right one. If a Flexbox row needs several fixed widths and spacer elements to preserve column alignment, Grid is probably the better owner. If a Grid layout uses many one-off areas only to place a row of buttons, Flexbox is probably enough.
Also check where the component will be reused. A layout that is correct in a full-width page section may fail in a sidebar because its breakpoint is tied to the viewport. In that case, the real choice may be Grid plus a container query, not Grid versus Flexbox alone.
The best production CSS often combines primitives in layers: Grid for page and card structure, Flexbox for small clusters, Subgrid for nested alignment, and normal flow for prose. The decision is not tribal. It is about assigning each relationship to the primitive that makes the next maintenance change obvious.