Twelve-Column Grid Component
Flex or grid 12-column layout with span and a single breakpoint. Spacing from a multiplier, and why 4+8 must wrap as 12+12 on small screens.
- machine-coding
- interview
- css
- react
Someone writes <div style={{ width: "33.33%" }}> next to a 66.66% sibling, adds gap: 16px, and the row overflows by 16px. Percent widths are a fraction of the container, not of “container minus gaps.” A twelve-column grid component treats span as “how many of 12 tracks,” and gap as space between tracks. 4 + 8 fills a desktop row. On the small breakpoint both items become 12, so they stack: 12 + 12, not a squeezed 4 + 8.
Interviewers are scoring the API (Grid / GridItem), the gap math, and one honest breakpoint. Not a Bootstrap clone.
Key takeaways
- 12 tracks.
spanis an integer 1–12. Sum of spans in a row should be 12 at the large breakpoint.- Gap is not width. CSS Grid
gapdoes the subtraction for you. Flex percentages do not.- One breakpoint is enough: below it, items span 12 unless you pass
spanSm.- Spacing from a multiplier (
gap={2}→2 * 8px), not magic pixels on every item.minmax(0, 1fr)prevents a child from blowing a track.
Span is a fraction of 12, gap is not width
CSS Grid is the default. Twelve columns, items occupy span tracks:
.fb-grid {
display: grid;
grid-template-columns: repeat(12, minmax(0, 1fr));
gap: var(--fb-gap, 8px);
}
.fb-grid-item {
grid-column: span var(--span-sm, 12);
}
@media (min-width: 640px) {
.fb-grid-item {
grid-column: span var(--span, 12);
}
}
Mobile-first: a 4-span item is full width on small screens (spanSm default 12) and four tracks from 640px up. Two children with span={4} and span={8} wrap as 12 + 12 below the breakpoint and sit on one row above it.
Flex version, if they forbid Grid. A full row is 12 column widths plus 11 gaps. An item that spans k columns owns k column widths and k - 1 gaps:
.fb-flex {
display: flex;
flex-wrap: wrap;
gap: var(--fb-gap);
}
.fb-flex-item {
flex: 0 0 auto;
width: calc(
(var(--span) / 12) * (100% - 11 * var(--fb-gap))
+ (var(--span) - 1) * var(--fb-gap)
);
}
That formula assumes the row’s spans add to 12. A leftover 3 + leftover gap on wrap is why you do not want flex unless forced. Grid tracks already include gap between them. Say this tradeoff and pick Grid.
gap vs padding: gap is between items. Horizontal padding on the container is page gutter, a different token. Do not mix them in one margin on GridItem.
Problem statement
Build Grid and GridItem for a 12-column layout. GridItem takes span (and optional small-span). Grid takes a spacing multiplier for gap. At a single breakpoint, items that share a row on desktop stack to full width. Demonstrate 4 + 8.
Requirements
Must have
Gridwith 12 columnsGridItemwithspan1–12- Gap from a spacing multiplier (e.g. 8px base)
- One breakpoint where default small span is 12
- 4 + 8 on large, stacked 12 + 12 on small
Should have
spanSmoverrideas/classNamepassthrough- Nested grids (a GridItem can host another Grid)
Nice to have
- Offset (start line)
- Two breakpoints
- Flex fallback
Planning (5 minutes out loud)
- Grid, not flex, unless they ban it
- CSS variables for span and gap so you do not generate 12 class names live
- Mobile-first media query
- MVP: Grid + GridItem + demo row
- Clamp span to 1–12, do not crash on 13
Architecture
Grid
└── GridItem × n
└── children
API
type GridProps = {
gap?: number; // multiplier, default 1
children: React.ReactNode;
className?: string;
};
type GridItemProps = {
span?: number; // large breakpoint, default 12
spanSm?: number; // default 12
children: React.ReactNode;
className?: string;
};
Implementation sketch
const BASE = 8;
function clampSpan(n: number) {
if (!Number.isFinite(n)) return 12;
return Math.min(12, Math.max(1, Math.round(n)));
}
export function Grid({ gap = 1, children, className }: GridProps) {
return (
<div
className={["fb-grid", className].filter(Boolean).join(" ")}
style={{ ["--fb-gap" as string]: `${gap * BASE}px` }}
>
{children}
</div>
);
}
export function GridItem({
span = 12,
spanSm = 12,
children,
className,
}: GridItemProps) {
return (
<div
className={["fb-grid-item", className].filter(Boolean).join(" ")}
style={{
["--span" as string]: clampSpan(span),
["--span-sm" as string]: clampSpan(spanSm),
}}
>
{children}
</div>
);
}
export function Demo() {
return (
<Grid gap={2}>
<GridItem span={4}>Nav</GridItem>
<GridItem span={8}>Main</GridItem>
</Grid>
);
}
gap={2} → 16px. The multiplier is the design-token story. Hardcoding 16px on the demo is weaker than showing the scale.
Nested grid: a GridItem’s child Grid is a new 12-track context the width of that item, not of the page. That is correct. Do not try to share tracks across nesting.
If they want offset: grid-column: var(--start) / span var(--span) with --start default auto. Skip unless asked.
Accessibility essentials
| Piece | Approach |
|---|---|
| Semantics | Grid is layout. Use main, nav, headings inside items |
| Order | DOM order should match visual order after wrap |
| Zoom | minmax(0, 1fr) plus wrapping beats overflow: hidden on text |
| Contrast | Not the component’s job; do not set tiny gray demo text as if it were |
Do not put role="grid" on a layout grid. That role is for tabular widgets, not page columns.
Interview scoring
| Signal | What they want |
|---|---|
| Tracks | 12 columns, span as grid-column |
| Gap | Token multiplier, not item margins |
| Math | You can explain why flex % + gap overflows |
| Breakpoint | 4+8 becomes 12+12, named in CSS |
| API | Two components, defaults that fill the row |
| Restraint | One breakpoint, no 12-file design system |
They may ask you to switch the demo to 3 + 3 + 6, then shrink the iframe. All three should stack if spanSm defaults to 12. If you used spanSm={span}, they stay in one cramped row. That is the bug.
Footguns
width: (span/12)*100%plusgap, overflowrepeat(12, 1fr)withoutminmax(0, …), long words explode the track- Desktop-first with no stacked default, 4+8 on a phone
role="grid"on a layout- Margin on items and gap, double spacing
- Span 0 or 13 unclamped
Interview out-loud answer
I would ship a Grid that is
display: gridwith twelveminmax(0, 1fr)columns and a gap from a spacing multiplier. GridItem setsgrid-column: span Nthrough CSS variables. Mobile-first, the small span defaults to 12 so a 4 + 8 pair wraps as two full-width rows, then at one breakpoint they sit on a single row. I would not use flex percentages, because gap is extra width those percentages do not know about. Layout grids do not getrole="grid".
Related on this site
- CSS Grid fundamentals
- minmax and fr units
- Gap vs margin in layouts
- Layout components
- Product Filters Sidebar