ESC

Type to search the knowledge base.

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.

intermediate6 min read
  • 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. span is an integer 1–12. Sum of spans in a row should be 12 at the large breakpoint.
  • Gap is not width. CSS Grid gap does 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

  • Grid with 12 columns
  • GridItem with span 1–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

  • spanSm override
  • as / className passthrough
  • Nested grids (a GridItem can host another Grid)

Nice to have

  • Offset (start line)
  • Two breakpoints
  • Flex fallback

Planning (5 minutes out loud)

  1. Grid, not flex, unless they ban it
  2. CSS variables for span and gap so you do not generate 12 class names live
  3. Mobile-first media query
  4. MVP: Grid + GridItem + demo row
  5. 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

  1. width: (span/12)*100% plus gap, overflow
  2. repeat(12, 1fr) without minmax(0, …), long words explode the track
  3. Desktop-first with no stacked default, 4+8 on a phone
  4. role="grid" on a layout
  5. Margin on items and gap, double spacing
  6. Span 0 or 13 unclamped

Interview out-loud answer

I would ship a Grid that is display: grid with twelve minmax(0, 1fr) columns and a gap from a spacing multiplier. GridItem sets grid-column: span N through 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 get role="grid".

Further reading