Grid Layouts Beta
Use display: grid with grid-template-columns to create structured column-row layouts — card grids, dashboards, invoice lines, sidebar layouts, and team directories — all from plain HTML and CSS.
Learning Objectives
By the end of this article, you’ll be able to:
- Create multi-column grids using
display: gridandgrid-template-columns - Use
frunits, fixed lengths,auto, andrepeat()to define columns - Control column spacing with
column-gap - Build common layout patterns: card grids, sidebars, dashboards, invoice lines
- Combine data binding with grid layouts for dynamic output
- Choose between grid, native columns, and table-cell for different scenarios
How Grid Layout Works
Setting display: grid on a container element activates grid layout. Its direct children are then placed into columns defined by grid-template-columns, filling left to right and wrapping to the next row automatically.
Container (display: grid; grid-template-columns: 1fr 1fr 1fr)
┌──────────────────────────────────────┐
│ Item 1 │ Item 2 │ Item 3 │ ← row 1
│ │ │ │
├──────────┼──────────┼───────────────┤
│ Item 4 │ Item 5 │ Item 6 │ ← row 2 (auto-wrapped)
│ │ │ │
└──────────────────────────────────────┘
Only direct children are placed in the grid. Grandchildren remain inside their parent cell.
Defining Columns
grid-template-columns accepts a space-separated list of track values. The number of values = number of columns.
fr units — proportional widths
fr (fraction) divides remaining space proportionally. After fixed columns and gaps are subtracted, remaining space is split by fr ratio.
grid-template-columns: 1fr 1fr; /* two equal columns — each 50% */
grid-template-columns: 1fr 2fr; /* 33% and 67% */
grid-template-columns: 1fr 1fr 1fr; /* three equal columns — each 33% */
grid-template-columns: 2fr 1fr 1fr; /* 50%, 25%, 25% */
Fixed lengths
Any CSS length unit works: pt, px, mm, cm, in, em, %.
grid-template-columns: 200pt 1fr; /* 200pt sidebar, rest to main content */
grid-template-columns: 25% 75%; /* quarter + three-quarters */
grid-template-columns: 80pt 80pt 80pt; /* three fixed columns */
auto
auto columns share remaining space equally. When no fr columns exist, each auto becomes 1fr.
grid-template-columns: auto auto auto; /* same as 1fr 1fr 1fr */
grid-template-columns: 200pt auto; /* fixed sidebar + auto-fill */
repeat()
repeat(count, track) expands to count copies of the track — saves repetition for uniform grids.
grid-template-columns: repeat(3, 1fr); /* 1fr 1fr 1fr */
grid-template-columns: repeat(4, 150pt); /* 150pt 150pt 150pt 150pt */
grid-template-columns: 200pt repeat(2, 1fr); /* 200pt 1fr 1fr */
Column Gap
column-gap adds whitespace between columns. The gap is subtracted from the available width before fr columns are calculated, so columns always fill the container perfectly.
.grid {
display: grid;
grid-template-columns: 1fr 1fr 1fr;
column-gap: 15pt; /* 15pt gap between each pair of adjacent columns */
}
For a 600pt container with 1fr 1fr and column-gap: 20pt:
- Available for columns = 600 − 20 = 580pt
- Each column = 580 / 2 = 290pt
Basic Examples
Three equal columns
<style>
.grid {
display: grid;
grid-template-columns: 1fr 1fr 1fr;
column-gap: 12pt;
}
.card {
padding: 15pt;
background-color: #f3f4f6;
border: 1pt solid #d1d5db;
}
.card h3 { margin: 0 0 8pt 0; color: #1e3a8a; }
.card p { margin: 0; font-size: 10pt; }
</style>
<body>
<div class="grid">
<div class="card">
<h3>Reliability</h3>
<p>99.9% uptime guaranteed across all services.</p>
</div>
<div class="card">
<h3>Security</h3>
<p>End-to-end encryption with audit logging.</p>
</div>
<div class="card">
<h3>Support</h3>
<p>Dedicated account manager for all customers.</p>
</div>
</div>
</body>
Two rows — six items, three columns
Items beyond three automatically wrap to a second row:
<style>
.team-grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
column-gap: 15pt;
}
.member {
text-align: center;
padding: 15pt;
margin-bottom: 15pt;
border: 1pt solid #e5e7eb;
background-color: #fafafa;
}
.name { font-weight: bold; color: #1f2937; }
.role { font-size: 10pt; color: #6b7280; margin-top: 4pt; }
</style>
<body>
<div class="team-grid">
<div class="member"><div class="name">Alice Chen</div><div class="role">Engineering</div></div>
<div class="member"><div class="name">Bob Smith</div><div class="role">Design</div></div>
<div class="member"><div class="name">Carol Lee</div><div class="role">Product</div></div>
<!-- wraps to row 2 -->
<div class="member"><div class="name">Dan Patel</div><div class="role">Marketing</div></div>
<div class="member"><div class="name">Eve Russo</div><div class="role">Sales</div></div>
<div class="member"><div class="name">Frank Oh</div><div class="role">Operations</div></div>
</div>
</body>
Common Layout Patterns
Sidebar + main content
A fixed sidebar beside a flexible content area:
<style>
.page-layout {
display: grid;
grid-template-columns: 160pt 1fr;
column-gap: 24pt;
}
.sidebar {
padding: 15pt;
background-color: #f8fafc;
border-right: 2pt solid #e2e8f0;
}
.sidebar h3 { margin: 0 0 12pt 0; font-size: 12pt; color: #475569; }
.sidebar p { margin: 0 0 8pt 0; font-size: 10pt; }
.content { padding: 15pt; }
</style>
<body>
<div class="page-layout">
<div class="sidebar">
<h3>Contents</h3>
<p>Introduction</p>
<p>Key Findings</p>
<p>Recommendations</p>
<p>Appendix</p>
</div>
<div class="content">
<h2>Executive Summary</h2>
<p>This report outlines our quarterly performance across all divisions...</p>
</div>
</div>
</body>
Dashboard metrics row
Four equal metric cards in a row:
<style>
.metrics {
display: grid;
grid-template-columns: repeat(4, 1fr);
column-gap: 12pt;
margin-bottom: 20pt;
}
.metric {
text-align: center;
padding: 18pt 10pt;
border: 1pt solid #e5e7eb;
border-top: 3pt solid #2563eb;
background-color: white;
}
.metric-value {
font-size: 26pt;
font-weight: bold;
color: #1e40af;
}
.metric-label {
font-size: 9pt;
color: #6b7280;
text-transform: uppercase;
margin-top: 6pt;
}
.metric-change {
font-size: 9pt;
color: #16a34a;
margin-top: 4pt;
}
</style>
<body>
<div class="metrics">
<div class="metric">
<div class="metric-value">£284K</div>
<div class="metric-label">Revenue</div>
<div class="metric-change">+12% vs last month</div>
</div>
<div class="metric">
<div class="metric-value">1,847</div>
<div class="metric-label">Customers</div>
<div class="metric-change">+8% vs last month</div>
</div>
<div class="metric">
<div class="metric-value">94%</div>
<div class="metric-label">Satisfaction</div>
<div class="metric-change">+2pts vs last month</div>
</div>
<div class="metric">
<div class="metric-value">23</div>
<div class="metric-label">Open Issues</div>
<div class="metric-change" style="color: #dc2626;">+3 this week</div>
</div>
</div>
</body>
Invoice line items
Fixed columns for a structured data layout — ideal for invoice tables without using <table> markup:
<style>
.invoice-header,
.invoice-line {
display: grid;
grid-template-columns: 1fr 55pt 75pt 80pt;
column-gap: 6pt;
}
.invoice-header {
background-color: #1f2937;
color: white;
margin-bottom: 1pt;
}
.invoice-line {
border-bottom: 1pt solid #f3f4f6;
}
.hcell { padding: 8pt 6pt; font-weight: bold; font-size: 10pt; }
.cell { padding: 7pt 6pt; font-size: 10pt; }
.right { text-align: right; }
.total-row {
display: grid;
grid-template-columns: 1fr 80pt;
column-gap: 6pt;
border-top: 2pt solid #1f2937;
margin-top: 6pt;
}
.total-label { padding: 8pt 6pt; font-weight: bold; text-align: right; }
.total-value { padding: 8pt 6pt; font-weight: bold; text-align: right; color: #1e40af; }
</style>
<body>
<div class="invoice-header">
<div class="hcell">Description</div>
<div class="hcell right">Qty</div>
<div class="hcell right">Unit Price</div>
<div class="hcell right">Amount</div>
</div>
<div class="invoice-line">
<div class="cell">Strategy consultancy — Q1 2025</div>
<div class="cell right">15</div>
<div class="cell right">£850.00</div>
<div class="cell right">£12,750.00</div>
</div>
<div class="invoice-line">
<div class="cell">Report writing and review</div>
<div class="cell right">8</div>
<div class="cell right">£650.00</div>
<div class="cell right">£5,200.00</div>
</div>
<div class="invoice-line">
<div class="cell">Workshop facilitation (half-day)</div>
<div class="cell right">2</div>
<div class="cell right">£1,200.00</div>
<div class="cell right">£2,400.00</div>
</div>
<div class="total-row">
<div class="total-label">Total</div>
<div class="total-value">£20,350.00</div>
</div>
</body>
Data Binding with Grid
Grid layout works naturally with {{#each}} loops. The grid wraps items automatically — you only define the column structure once.
Product catalogue
<style>
.catalogue {
display: grid;
grid-template-columns: repeat(3, 1fr);
column-gap: 12pt;
}
.product {
padding: 12pt;
border: 1pt solid #e5e7eb;
margin-bottom: 12pt;
background-color: white;
}
.product-name { font-weight: bold; color: #1e3a8a; margin-bottom: 6pt; }
.product-price { color: #16a34a; font-size: 14pt; font-weight: bold; }
.product-desc { font-size: 9pt; color: #6b7280; margin-top: 4pt; }
</style>
<body>
<h1>{{catalogue.title}}</h1>
<div class="catalogue">
{{#each catalogue.products}}
<div class="product">
<div class="product-name">{{this.name}}</div>
<div class="product-price">{{this.price}}</div>
<div class="product-desc">{{this.description}}</div>
</div>
{{/each}}
</div>
</body>
Status report with conditional colouring
<style>
.report-grid {
display: grid;
grid-template-columns: 2fr 1fr 1fr 1fr;
column-gap: 8pt;
}
.header-row, .data-row {
display: contents; /* let children participate in parent grid */
}
.hcell {
padding: 8pt;
background-color: #374151;
color: white;
font-weight: bold;
font-size: 10pt;
margin-bottom: 2pt;
}
.dcell {
padding: 8pt;
font-size: 10pt;
border-bottom: 1pt solid #f3f4f6;
}
.status-green { color: #16a34a; font-weight: bold; }
.status-amber { color: #d97706; font-weight: bold; }
.status-red { color: #dc2626; font-weight: bold; }
</style>
<body>
<div class="report-grid">
<div class="hcell">Project</div>
<div class="hcell">Status</div>
<div class="hcell">Budget</div>
<div class="hcell">ETC</div>
{{#each projects}}
<div class="dcell">{{this.name}}</div>
<div class="dcell status-{{this.statusColor}}">{{this.status}}</div>
<div class="dcell">{{this.budgetUsed}}%</div>
<div class="dcell">{{this.estimatedCompletion}}</div>
{{/each}}
</div>
</body>
Choosing the Right Layout
| Scenario | Best choice | Why |
|---|---|---|
| Body text flowing across equal columns | column-count |
Text reflows automatically across columns |
| Fixed set of cards or items in rows | display: grid |
Explicit column widths, items stay in cells |
| Sidebar + main content | display: grid |
Mixed fixed/flexible column widths |
| Dashboard metric cards | display: grid |
Uniform columns, clean alignment |
| Invoice / data rows | display: grid |
Precise column widths match headers to data |
| Simple two-column equal height | display: table-cell |
Lighter markup for basic side-by-side |
| Text wrapping around an image | float |
Image stays inline with surrounding text |
| Equal-height columns with ruled dividers | column-count + column-rule |
Rule lines between text columns |
Container Sizing
By default, a grid container is block-level and fills the available width. You can also give it an explicit width:
/* Full width (default) */
.grid { display: grid; grid-template-columns: 1fr 1fr; }
/* Explicit width — columns are sized within this */
.grid { display: grid; grid-template-columns: 1fr 1fr; width: 400pt; }
/* Width and margin — margin reduces the available width for fr columns */
.grid { display: grid; grid-template-columns: 1fr 1fr; margin-left: 30pt; }
Container padding also reduces column width — the columns are calculated within the padded inner area:
.grid {
display: grid;
grid-template-columns: 1fr 1fr;
padding: 20pt; /* columns share (container-width − 2×20pt) */
}
Notes and Limitations
- Only direct children of the grid container are placed in columns — nested elements stay inside their cell
- Items fill columns left to right, top to bottom (row-major order) — there is no column-major mode
- No explicit item placement — you cannot use
grid-columnorgrid-rowto position items; placement is always automatic - No row spanning — items cannot span multiple rows
display: flexis not supported — usedisplay: gridfor structured column layouts- Partial rows are allowed — a 3-column grid with 5 items produces two rows: [1,2,3] and [4,5]
- Each item renders with its own
border,padding,height, andbackgroundas set on the item itself column-gapadds space between columns only — it does not add space before column 1 or after the last column (use containerpaddingfor that)
Quick Reference
| Property | What it does |
|---|---|
display: grid |
Activate grid layout on the container |
grid-template-columns: 1fr 1fr |
Two equal columns |
grid-template-columns: 200pt 1fr |
Fixed + flexible |
grid-template-columns: repeat(3, 1fr) |
Three equal columns |
column-gap: 15pt |
Space between columns |
Item height: 80pt |
Fixed height for all cells in that row |
Item padding: 12pt |
Inner spacing within each cell |
Item border: 1pt solid #ccc |
Border around each cell |
See Also
- Multi-Column Layouts — flowing text across balanced columns
- Table Layouts — data tables with
<table>markup displayproperty reference — all display modesgrid-template-columnsreference — full column syntaxcolumn-gapreference — gap sizing
← Previous: Layout Best Practices