Overview
The Grid is a two-dimensional layout system that organizes content into rows and columns simultaneously. Unlike Stack (which operates on a single axis using Flexbox), Grid leverages CSS Grid Layout to provide precise control over both horizontal and vertical placement, spanning, alignment, and sizing — all declaratively.
Grid is the backbone of page-level layout and complex component arrangements. It replaces the float-based and framework-specific 12-column grids that dominated web development for over a decade with a browser-native, infinitely flexible system.
When to use a Grid:
- For page-level layout (header, sidebar, main content, footer)
- For card grids that need uniform sizing across rows and columns
- For dashboard layouts with widgets of varying sizes
- For form layouts where labels and inputs need column alignment
- For any layout requiring both row and column control simultaneously
When NOT to use a Grid:
- For single-axis stacking — use a Stack with Flexbox
- For simple centering — Flexbox is simpler
- For flowing inline content — use normal document flow
- For layout that should reorder based on content priority — Flexbox's
orderand wrapping may be more intuitive
Experiment with column counts, gap values, and responsive breakpoints using the Grid Generator. Fine-tune your gap and padding values with the Spacing Scale Generator.
Variants
Grid Variants
| Variant | Description | CSS Technique |
|---|---|---|
| Fixed columns | A set number of columns at every viewport size. | grid-template-columns: repeat(3, 1fr) |
| Responsive auto-fill | Columns automatically created to fill available space. Items wrap to new rows. | grid-template-columns: repeat(auto-fill, minmax(250px, 1fr)) |
| Responsive auto-fit | Like auto-fill but collapses empty tracks, stretching items to fill. | grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)) |
| Asymmetric | Columns with different widths (e.g., sidebar + content). | grid-template-columns: 240px 1fr or grid-template-columns: 1fr 2fr 1fr |
| Named areas | Columns and rows defined by named template areas for readable layouts. | grid-template-areas: "header header" "sidebar main" "footer footer" |
| Masonry | Items fill vertical space optimally (Pinterest-style). | grid-template-rows: masonry (experimental) or JS-based |
| Subgrid | Child grid inherits parent's track sizing. | grid-template-columns: subgrid |
auto-fill vs auto-fit
This is the most commonly confused Grid distinction:
| Behavior | auto-fill | auto-fit |
|---|---|---|
| Extra space | Creates empty tracks, items stay at minmax size | Collapses empty tracks, items stretch to fill |
| Few items | Items stay small, empty columns visible | Items stretch to fill the row |
| Many items | Identical behavior | Identical behavior |
| Best for | Uniform sizing regardless of item count | Filling available space when item count varies |
Gap Variants
| Gap Size | Value | Use Case |
|---|---|---|
| Tight | 8px | Dense dashboards, thumbnail grids |
| Default | 16px | Standard card grids, form layouts |
| Relaxed | 24px | Marketing pages, content-heavy grids |
| Loose | 32–48px | Hero-level layouts, spacious designs |
Preview these gap values in context with the Spacing Scale Generator.
Properties
Grid Properties
| Property | Type | Default | Description |
|---|---|---|---|
columns | number | string | 12 | Number of columns or a CSS grid-template-columns value |
rows | number | string | 'auto' | Number of rows or a CSS grid-template-rows value |
gap | number | string | 16 | Uniform gap between rows and columns (px or token) |
columnGap | number | string | — | Override gap for columns only |
rowGap | number | string | — | Override gap for rows only |
areas | string[] | — | Array of template area strings (each string is one row) |
autoFlow | 'row' | 'column' | 'dense' | 'row dense' | 'column dense' | 'row' | Controls auto-placement algorithm direction |
autoColumns | string | — | Size for implicitly created columns |
autoRows | string | — | Size for implicitly created rows |
alignItems | 'start' | 'end' | 'center' | 'stretch' | 'baseline' | 'stretch' | Vertical alignment of items within their grid area |
justifyItems | 'start' | 'end' | 'center' | 'stretch' | 'stretch' | Horizontal alignment of items within their grid area |
alignContent | string | — | Alignment of the entire grid within its container (vertical) |
justifyContent | string | — | Alignment of the entire grid within its container (horizontal) |
minChildWidth | string | — | Shorthand: generates repeat(auto-fit, minmax(value, 1fr)) |
inline | boolean | false | Renders as inline-grid instead of grid |
Grid Item Properties
| Property | Type | Default | Description |
|---|---|---|---|
colSpan | number | 1 | Number of columns the item spans |
rowSpan | number | 1 | Number of rows the item spans |
colStart | number | — | Starting column line |
colEnd | number | — | Ending column line |
rowStart | number | — | Starting row line |
rowEnd | number | — | Ending row line |
area | string | — | Named grid area this item occupies |
alignSelf | string | — | Override vertical alignment for this item |
justifySelf | string | — | Override horizontal alignment for this item |
Token Mappings
Design Token Mappings
| Token | Role | Typical Value |
|---|---|---|
space.1 | Tight gap | 4px |
space.2 | Small gap | 8px |
space.3 | Compact gap | 12px |
space.4 | Default gap | 16px |
space.6 | Relaxed gap | 24px |
space.8 | Loose gap | 32px |
space.12 | Spacious gap | 48px |
breakpoint.sm | Small viewport threshold | 640px |
breakpoint.md | Medium viewport threshold | 768px |
breakpoint.lg | Large viewport threshold | 1024px |
breakpoint.xl | Extra-large viewport threshold | 1280px |
size.container.sm | Narrow content width | 640px |
size.container.md | Default content width | 768px |
size.container.lg | Wide content width | 1024px |
size.container.xl | Full content width | 1280px |
size.sidebar | Sidebar width in asymmetric grids | 240–280px |
Visualise these values using the Spacing Scale Generator and experiment with column configurations in the Grid Generator.
States
Grid States
Grid is a layout primitive with no interactive states of its own. However, responsive behavior constitutes a form of state driven by viewport conditions:
| State | Trigger | Effect |
|---|---|---|
| Default | Initial render | Grid renders with specified columns, gap, and areas |
| Responsive breakpoint | Viewport crosses a breakpoint threshold | Column count, gap, and/or areas change. Items reflow. |
| Dense packing | autoFlow: 'dense' | Auto-placement algorithm fills gaps by reordering items visually (caution: creates a11y issues) |
| Overflow | Content exceeds grid area | Items overflow their cells. Handled by item-level overflow styles, not the grid. |
| Subgrid inheritance | Child uses subgrid | Child grid tracks align with parent tracks |
Responsive Patterns
The most common responsive pattern transforms a multi-column grid into a single-column stack on mobile:
.grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(min(100%, 300px), 1fr));
gap: var(--space-4);
}
The min(100%, 300px) pattern prevents horizontal overflow on viewports narrower than the minimum item width — a critical detail many implementations miss.
Important a11y note: grid-auto-flow: dense and explicit order changes can cause visual order to diverge from DOM order. Screen readers and keyboard users follow DOM order. Per WCAG 2.4.3 (Focus Order) and 1.3.2 (Meaningful Sequence), visual reordering must not create a confusing navigation experience.
Accessibility
Accessibility Requirements
Grid is a CSS layout mechanism, not a semantic structure. The grid container and items carry no implicit ARIA semantics — accessibility depends entirely on the semantic HTML placed within grid cells.
WCAG Success Criteria:
- 1.3.1 Info and Relationships (Level A): Grid visual structure does not convey semantic relationships. Use proper headings, landmarks (
<nav>,<main>,<aside>), and lists within grid cells to express structure programmatically. - 1.3.2 Meaningful Sequence (Level A): The DOM order must make sense when read linearly, regardless of visual placement. Avoid using
grid-column,grid-row, ororderto visually reorder content in ways that contradict the reading sequence. - 2.4.3 Focus Order (Level A): Tab order follows DOM order, not visual order. If you visually place a sidebar before main content using Grid but the sidebar appears after main content in the DOM, keyboard navigation will seem to jump randomly. Always match DOM order to intended reading order.
- 1.4.10 Reflow (Level AA): At 320px viewport width (or 400% zoom), content must reflow into a single column without horizontal scrolling. Grid layouts must gracefully degrade using
auto-fit/auto-fillor media queries. Verify with the Grid Generator. - 1.4.4 Resize Text (Level AA): Text resized up to 200% must not be clipped or overlapped by fixed grid track sizes. Use
frunits andminmax()instead of fixed pixel widths for content tracks.
Landmark Usage in Grid Layouts:
<div class="page-grid">
<header>...</header>
<nav aria-label="Main">...</nav>
<main>...</main>
<aside aria-label="Related">...</aside>
<footer>...</footer>
</div>
The grid provides visual positioning; the HTML5 elements provide semantic structure. Never rely on Grid alone to communicate layout meaning. Validate your divider and separator contrast ratios with the Contrast Checker.
Usage Guidelines
Usage Guidelines
Do:
- Use
auto-fitorauto-fillwithminmax()for responsive card grids that adapt without media queries - Use named grid areas for complex page layouts — they are self-documenting and easier to maintain
- Use
frunits for flexible tracks and reserve pixel/rem values for fixed elements (sidebars, gutters) - Ensure DOM order matches visual reading order to maintain accessible navigation (WCAG 1.3.2, 2.4.3)
- Use the
min(100%, <desired-width>)pattern insideminmax()to prevent overflow on narrow viewports - Combine with CSS
container queriesfor component-level responsive grids that adapt to their container, not just the viewport
Don't:
- Use CSS Grid for simple single-axis layouts — a Stack with Flexbox is simpler and more semantic
- Use
grid-auto-flow: densein content where reading order matters — it reorders items visually without changing the DOM - Set fixed pixel heights on grid rows containing dynamic content — use
autoormin-content - Create grid layouts that require horizontal scrolling at any common viewport width (WCAG 1.4.10)
- Mix Grid and Flexbox on the same container — choose one layout mode per element
- Forget to test with keyboard navigation when using visual reordering — focus order must remain logical
Responsive Strategy:
| Viewport | Columns | Gap | Pattern |
|---|---|---|---|
| < 640px | 1 | 16px | Single column stack |
| 640–1023px | 2 | 16–24px | Compact grid |
| 1024–1279px | 3 | 24px | Standard grid |
| ≥ 1280px | 4+ | 24–32px | Full grid |
Code Snippets
<!-- Responsive auto-fit grid -->
<div class="grid" style="
display: grid;
grid-template-columns: repeat(auto-fit, minmax(min(100%, 280px), 1fr));
gap: 24px;
">
<div class="card">Card 1</div>
<div class="card">Card 2</div>
<div class="card">Card 3</div>
<div class="card">Card 4</div>
</div>
<!-- Page layout with named areas -->
<div class="page-grid" style="
display: grid;
grid-template-areas:
'header header'
'sidebar main'
'footer footer';
grid-template-columns: 260px 1fr;
grid-template-rows: auto 1fr auto;
min-height: 100vh;
gap: 0;
">
<header style="grid-area: header;">Header</header>
<nav style="grid-area: sidebar;" aria-label="Main navigation">Sidebar</nav>
<main style="grid-area: main;">Content</main>
<footer style="grid-area: footer;">Footer</footer>
</div>
<!-- Dashboard grid with spanning items -->
<div class="dashboard-grid" style="
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 16px;
">
<div style="grid-column: span 2; grid-row: span 2;">Large widget</div>
<div>Small widget 1</div>
<div>Small widget 2</div>
<div>Small widget 3</div>
<div>Small widget 4</div>
</div>
<!-- Responsive: collapse sidebar on mobile -->
<style>
@media (max-width: 768px) {
.page-grid {
grid-template-areas:
'header'
'main'
'footer' !important;
grid-template-columns: 1fr !important;
}
.page-grid nav { display: none; }
}
</style>import React from 'react';
interface GridProps {
columns?: number | string;
rows?: string;
gap?: number | string;
columnGap?: number | string;
rowGap?: number | string;
areas?: string[];
autoFlow?: 'row' | 'column' | 'dense' | 'row dense' | 'column dense';
autoRows?: string;
autoColumns?: string;
alignItems?: 'start' | 'end' | 'center' | 'stretch' | 'baseline';
justifyItems?: 'start' | 'end' | 'center' | 'stretch';
minChildWidth?: string;
inline?: boolean;
children: React.ReactNode;
className?: string;
style?: React.CSSProperties;
}
function Grid({
columns = 12,
rows,
gap = 16,
columnGap,
rowGap,
areas,
autoFlow,
autoRows,
autoColumns,
alignItems,
justifyItems,
minChildWidth,
inline = false,
children,
className,
style,
}: GridProps) {
const templateColumns = minChildWidth
? `repeat(auto-fit, minmax(min(100%, ${minChildWidth}), 1fr))`
: typeof columns === 'number'
? `repeat(${columns}, 1fr)`
: columns;
return (
<div
className={className}
style={{
display: inline ? 'inline-grid' : 'grid',
gridTemplateColumns: templateColumns,
gridTemplateRows: rows,
gridTemplateAreas: areas?.map(a => `"${a}"`).join(' '),
gap: typeof gap === 'number' ? `${gap}px` : gap,
columnGap: columnGap != null ? (typeof columnGap === 'number' ? `${columnGap}px` : columnGap) : undefined,
rowGap: rowGap != null ? (typeof rowGap === 'number' ? `${rowGap}px` : rowGap) : undefined,
gridAutoFlow: autoFlow,
gridAutoRows: autoRows,
gridAutoColumns: autoColumns,
alignItems,
justifyItems,
...style,
}}
>
{children}
</div>
);
}
interface GridItemProps {
colSpan?: number;
rowSpan?: number;
colStart?: number;
rowStart?: number;
area?: string;
children: React.ReactNode;
className?: string;
}
function GridItem({ colSpan, rowSpan, colStart, rowStart, area, children, className }: GridItemProps) {
return (
<div
className={className}
style={{
gridColumn: colSpan ? `span ${colSpan}` : undefined,
gridRow: rowSpan ? `span ${rowSpan}` : undefined,
gridColumnStart: colStart,
gridRowStart: rowStart,
gridArea: area,
}}
>
{children}
</div>
);
}
// Usage: Dashboard
function Dashboard() {
return (
<Grid columns={4} gap={16}>
<GridItem colSpan={2} rowSpan={2}>
<div className="widget widget--large">Revenue Chart</div>
</GridItem>
<GridItem><div className="widget">Users Online</div></GridItem>
<GridItem><div className="widget">Conversion Rate</div></GridItem>
<GridItem colSpan={2}><div className="widget">Recent Orders</div></GridItem>
</Grid>
);
}
// Usage: Responsive card grid
function CardGrid({ cards }: { cards: { id: string; title: string }[] }) {
return (
<Grid minChildWidth="280px" gap={24}>
{cards.map(card => (
<div key={card.id} className="card">{card.title}</div>
))}
</Grid>
);
}Design Systems
Design System Implementations
Material Design 3 (MUI) provides <Grid> (v1, based on Flexbox with a 12-column model) and the newer <Grid2> (also Flexbox-based). MUI does not expose a CSS Grid component natively — its Grid is a Flexbox wrapper with container/item props, xs/sm/md/lg/xl breakpoint sizing, spacing (gap), and direction. For true CSS Grid, MUI users typically use the <Box> component with display="grid" and pass grid properties directly. MUI's Grid2 (unstable) removes the container/item distinction, using size and offset props instead.
Ant Design provides a <Row>/<Col> grid system modeled after Bootstrap's 24-column grid. Props include gutter (gap, supports responsive object { xs: 8, md: 16 }), justify, align, wrap, and per-Col props span, offset, push, pull, and responsive breakpoints (xs, sm, md, lg, xl, xxl). This is Flexbox-based, not CSS Grid.
Chakra UI provides <Grid> (CSS Grid) and <SimpleGrid> (convenience wrapper). Grid accepts templateColumns, templateRows, gap, autoFlow, autoRows, autoColumns, and all grid CSS properties via style props. SimpleGrid offers columns (fixed column count) and minChildWidth (generates auto-fit/minmax). GridItem accepts colSpan, rowSpan, colStart, colEnd, rowStart, rowEnd, area. Chakra's grid is the most CSS Grid-native implementation in mainstream libraries.
Radix UI does not provide a grid component — layout is considered application-level rather than a headless primitive concern.
Headless UI does not include layout primitives.
Tailwind CSS provides grid utilities directly: grid, grid-cols-{n}, gap-{n}, col-span-{n}, row-span-{n}, auto-cols-{size}, auto-rows-{size}, and responsive variants. Combined with @apply or component abstractions, Tailwind's utility approach is the most flexible for custom grids. The utility grid-cols-[repeat(auto-fit,minmax(280px,1fr))] enables responsive grids without media queries.