Grid
A flexible CSS Grid layout component with element-width responsive columns, named grid areas, and fine-grained item placement. Perfect for dashboards, card grids, photo galleries, and complex page layouts.
Features
- 📐 12-Column System: Fixed 1–12 column layouts plus auto-fit mode
- 📱 Element-Width Responsive: Breakpoints respond to the element's own width via ResizeObserver — works correctly inside sidebars, modals, and nested layouts
- 🎯 Explicit Breakpoint Control: Set columns at sm, md, lg, and xl widths with dedicated attributes
- 📏 Row Support: Define explicit row layouts for dashboard grids
- 🗺️ Named Grid Areas: Use
areasto define named regions directly on the grid - 🔄 Flow Control: Row, column, and dense packing modes
- 📊 7 Gap Sizes: From
noneto2xlwith separate row/column gap support - 🧲 Alignment Control: Align and justify items with CSS Grid properties
- 🎨 Grid Item Component: Precise placement with
bit-grid-itemusing spans or raw CSS grid shorthand - 🔧 Customizable: CSS custom properties available as fallbacks
Source Code
View Source Code (Grid)
import { defineComponent, effect, html, onMount } from '@vielzeug/craftit';
import { observeResize } from '@vielzeug/craftit/labs';
const BREAKPOINTS: ['cols2xl' | 'colsXl' | 'colsLg' | 'colsMd' | 'colsSm', string][] = [
['cols2xl', '--size-screen-2xl'],
['colsXl', '--size-screen-xl'],
['colsLg', '--size-screen-lg'],
['colsMd', '--size-screen-md'],
['colsSm', '--size-screen-sm'],
];
const AREAS_BREAKPOINTS: ['areas2xl' | 'areasXl' | 'areasLg' | 'areasMd' | 'areasSm', string][] = [
['areas2xl', '--size-screen-2xl'],
['areasXl', '--size-screen-xl'],
['areasLg', '--size-screen-lg'],
['areasMd', '--size-screen-md'],
['areasSm', '--size-screen-sm'],
];
const resolveBp = (host: HTMLElement, varName: string, fallback: number): number => {
const raw = getComputedStyle(host).getPropertyValue(varName).trim();
const parsed = Number.parseFloat(raw);
return Number.isFinite(parsed) ? parsed : fallback;
};
const BP_FALLBACKS: Record<string, number> = {
'--size-screen-2xl': 1536,
'--size-screen-lg': 1024,
'--size-screen-md': 768,
'--size-screen-sm': 640,
'--size-screen-xl': 1280,
};
import styles from './grid.css?inline';
type ColCount = '1' | '2' | '3' | '4' | '5' | '6' | '7' | '8' | '9' | '10' | '11' | '12' | 'auto';
/** Grid component properties */
export type BitGridProps = {
/** Align items vertically */
align?: 'start' | 'center' | 'end' | 'stretch' | 'baseline';
/** CSS grid-template-areas value */
areas?: string;
/** grid-template-areas at 2xl breakpoint (≥1536px) */
areas2xl?: string;
/** grid-template-areas at lg breakpoint (≥1024px) */
areasLg?: string;
/** grid-template-areas at md breakpoint (≥768px) */
areasMd?: string;
/** grid-template-areas at sm breakpoint (≥640px) */
areasSm?: string;
/** grid-template-areas at xl breakpoint (≥1280px) */
areasXl?: string;
/** Number of columns: '1'-'12' | 'auto' */
cols?: ColCount;
/** Columns at 2xl breakpoint (≥1536px) */
cols2xl?: ColCount;
/** Columns at lg breakpoint (≥1024px) */
colsLg?: ColCount;
/** Columns at md breakpoint (≥768px) */
colsMd?: ColCount;
/** Columns at sm breakpoint (≥640px) */
colsSm?: ColCount;
/** Columns at xl breakpoint (≥1280px) */
colsXl?: ColCount;
/** Grid auto flow direction */
flow?: 'row' | 'column' | 'row-dense' | 'column-dense';
/** Stretch the grid to fill its container's full width */
fullwidth?: boolean;
/** Gap between items */
gap?: 'none' | 'xs' | 'sm' | 'md' | 'lg' | 'xl' | '2xl';
/** Justify items horizontally */
justify?: 'start' | 'center' | 'end' | 'stretch';
/** Minimum column width for responsive mode (default: 250px) */
minColWidth?: string;
/** Use auto-fit responsive columns */
responsive?: boolean;
/** Number of rows: '1'-'12' | 'auto' */
rows?: '1' | '2' | '3' | '4' | '5' | '6' | '7' | '8' | '9' | '10' | '11' | '12' | 'auto';
};
/**
* bit-grid — Flexible grid layout with responsive column control.
*
* Columns are computed in JS and applied as `--_cols` inline, so they respond
* to the element's own width via ResizeObserver. CSS custom properties
* `--grid-cols`, `--grid-rows`, `--grid-gap`, `--grid-row-gap`, `--grid-col-gap`
* are honoured as fallbacks when no attribute is set.
*
* @element bit-grid
*
* @attr {string} cols - Column count: '1'–'12' | 'auto'
* @attr {string} cols-sm - Columns when width ≥ 640px
* @attr {string} cols-md - Columns when width ≥ 768px
* @attr {string} cols-lg - Columns when width ≥ 1024px
* @attr {string} cols-xl - Columns when width ≥ 1280px
* @attr {string} cols-2xl - Columns when width ≥ 1536px
* @attr {string} rows - Row count: '1'–'12' | 'auto'
* @attr {string} gap - Gap token: 'none' | 'xs' | 'sm' | 'md' | 'lg' | 'xl' | '2xl'
* @attr {string} align - align-items: 'start' | 'center' | 'end' | 'stretch' | 'baseline'
* @attr {string} justify - justify-items: 'start' | 'center' | 'end' | 'stretch'
* @attr {string} flow - grid-auto-flow: 'row' | 'column' | 'row-dense' | 'column-dense'
* @attr {boolean} responsive - Enables auto-fit columns without a fixed count
* @attr {string} min-col-width - Min column width for responsive mode (default: 250px)
* @attr {boolean} fullwidth - Stretch the grid to fill its container's full width
* @attr {string} areas - CSS grid-template-areas value (e.g. "'header header' 'nav main'")
* @attr {string} areas-sm - grid-template-areas when width ≥ 640px
* @attr {string} areas-md - grid-template-areas when width ≥ 768px
* @attr {string} areas-lg - grid-template-areas when width ≥ 1024px
* @attr {string} areas-xl - grid-template-areas when width ≥ 1280px
* @attr {string} areas-2xl - grid-template-areas when width ≥ 1536px
*
* @slot - Grid items
*
* @cssprop --grid-cols - Fallback column template (used when no cols attr is set)
* @cssprop --grid-rows - Fallback row template
* @cssprop --grid-gap - Fallback gap
* @cssprop --grid-row-gap - Fallback row gap
* @cssprop --grid-col-gap - Fallback column gap
*
* @example
* <bit-grid cols="1" cols-sm="2" cols-lg="4" gap="md">
* <div>Item</div>
* </bit-grid>
*
* @example
* <!-- Responsive auto-fit -->
* <bit-grid responsive min-col-width="200px" gap="sm">
* <div>Card</div>
* </bit-grid>
*
* @example
* <!-- Named grid areas -->
* <bit-grid cols="2" rows="2" areas="'header header' 'nav main'">
* <header style="grid-area: header">Header</header>
* <nav style="grid-area: nav">Nav</nav>
* <main style="grid-area: main">Main</main>
* </bit-grid>
*/
export const GRID_TAG = defineComponent<BitGridProps>({
props: {
align: { default: undefined },
areas: { default: '' },
areas2xl: { default: '' },
areasLg: { default: '' },
areasMd: { default: '' },
areasSm: { default: '' },
areasXl: { default: '' },
cols: { default: undefined },
cols2xl: { default: undefined },
colsLg: { default: undefined },
colsMd: { default: undefined },
colsSm: { default: undefined },
colsXl: { default: undefined },
flow: { default: undefined },
fullwidth: { default: false },
gap: { default: undefined },
justify: { default: undefined },
minColWidth: { default: '' },
responsive: { default: false },
rows: { default: undefined },
},
setup({ host, props }) {
const computeCols = (activeCols: string | undefined, responsive: boolean, minW: string): string | null => {
if (activeCols === 'auto' || (!activeCols && responsive)) {
return `repeat(auto-fit, minmax(${minW || '250px'}, 1fr))`;
}
return activeCols ? `repeat(${activeCols}, 1fr)` : null;
};
const updateCols = () => {
const w = host.offsetWidth;
const responsive = Boolean(props.responsive.value);
const minW = props.minColWidth.value ?? '';
let activeCols: string | undefined;
for (const [key, cssVar] of BREAKPOINTS) {
if (w >= resolveBp(host, cssVar, BP_FALLBACKS[cssVar]) && props[key].value) {
activeCols = props[key].value!;
break;
}
}
activeCols ||= props.cols.value || undefined;
const colsValue = computeCols(activeCols, responsive, minW);
// eslint-disable-next-line @typescript-eslint/no-unused-expressions
colsValue ? host.style.setProperty('--_cols', colsValue) : host.style.removeProperty('--_cols');
};
// Re-run cols whenever any responsive prop changes
effect(() => {
void [
props.cols.value,
props.colsSm.value,
props.colsMd.value,
props.colsLg.value,
props.colsXl.value,
props.cols2xl.value,
props.responsive.value,
props.minColWidth.value,
];
updateCols();
});
const updateAreas = () => {
const w = host.offsetWidth;
let active = '';
for (const [key, cssVar] of AREAS_BREAKPOINTS) {
if (w >= resolveBp(host, cssVar, BP_FALLBACKS[cssVar]) && props[key].value) {
active = props[key].value!;
break;
}
}
active ||= props.areas.value || '';
// eslint-disable-next-line @typescript-eslint/no-unused-expressions
active ? host.style.setProperty('grid-template-areas', active) : host.style.removeProperty('grid-template-areas');
};
// Also, update on element resize (drives breakpoint switching)
onMount(() => {
const size = observeResize(host);
effect(() => {
void size.value;
updateCols();
updateAreas();
});
});
// Rows
effect(() => {
const rows = props.rows.value;
// eslint-disable-next-line @typescript-eslint/no-unused-expressions
rows && rows !== 'auto'
? host.style.setProperty('--_rows', `repeat(${rows}, 1fr)`)
: host.style.removeProperty('--_rows');
});
// Grid template areas (responsive)
effect(() => {
void [
props.areas.value,
props.areasSm.value,
props.areasMd.value,
props.areasLg.value,
props.areasXl.value,
props.areas2xl.value,
];
updateAreas();
});
return html`<slot></slot>`;
},
styles: [styles],
tag: 'bit-grid',
});View Source Code (Grid Item)
import { defineComponent, effect, html } from '@vielzeug/craftit';
import styles from './grid-item.css?inline';
/** Grid item component properties */
export type BitGridItemProps = {
/** Align self vertically within the grid cell */
align?: 'start' | 'center' | 'end' | 'stretch';
/** Explicit grid-column value — overrides col-span (e.g. '2 / 5', 'span 3', '1 / -1'). */
col?: string;
/** Span N columns. Use 'full' to span all columns (1 / -1). */
colSpan?: '1' | '2' | '3' | '4' | '5' | '6' | '7' | '8' | '9' | '10' | '11' | '12' | 'full';
/** Justify self horizontally within the grid cell */
justify?: 'start' | 'center' | 'end' | 'stretch';
/** Explicit grid-row value — overrides row-span (e.g. '1 / 3', 'span 2'). */
row?: string;
/** Span N rows. Use 'full' to span all rows (1 / -1). */
rowSpan?: '1' | '2' | '3' | '4' | '5' | '6' | 'full';
};
/**
* bit-grid-item — A grid cell with declarative placement and span control.
*
* Use `col-span` / `row-span` for the common case of spanning columns/rows.
* Use `col` / `row` for full CSS grid-column / grid-row shorthand power
* (e.g. explicit placement, mixed span + start, negative lines).
*
* @element bit-grid-item
*
* @attr {string} col-span - Columns to span: '1'–'12' | 'full'
* @attr {string} row-span - Rows to span: '1'–'6' | 'full'
* @attr {string} col - CSS grid-column value (overrides col-span)
* @attr {string} row - CSS grid-row value (overrides row-span)
* @attr {string} align - align-self: 'start' | 'center' | 'end' | 'stretch'
* @attr {string} justify - justify-self: 'start' | 'center' | 'end' | 'stretch'
*
* @slot - Grid item content
*
* @example
* <!-- Span 2 columns -->
* <bit-grid-item col-span="2">Wide</bit-grid-item>
*
* @example
* <!-- Full-width row -->
* <bit-grid-item col-span="full">Banner</bit-grid-item>
*
* @example
* <!-- Explicit placement -->
* <bit-grid-item col="2 / 5" row="1 / 3">Placed</bit-grid-item>
*/
export const GRID_ITEM_TAG = defineComponent<BitGridItemProps>({
props: {
align: { default: undefined },
col: { default: '' },
colSpan: { default: undefined },
justify: { default: undefined },
row: { default: '' },
rowSpan: { default: undefined },
},
setup({ host, props }) {
effect(() => {
const col = props.col.value;
const span = props.colSpan.value;
if (col) {
host.style.setProperty('grid-column', col);
} else if (span === 'full') {
host.style.setProperty('grid-column', '1 / -1');
} else if (span) {
host.style.setProperty('grid-column', `span ${span}`);
} else {
host.style.removeProperty('grid-column');
}
});
effect(() => {
const row = props.row.value;
const span = props.rowSpan.value;
if (row) {
host.style.setProperty('grid-row', row);
} else if (span === 'full') {
host.style.setProperty('grid-row', '1 / -1');
} else if (span) {
host.style.setProperty('grid-row', `span ${span}`);
} else {
host.style.removeProperty('grid-row');
}
});
return html`<slot></slot>`;
},
styles: [styles],
tag: 'bit-grid-item',
});Basic Usage
<bit-grid cols="3" gap="md">
<bit-card>Item 1</bit-card>
<bit-card>Item 2</bit-card>
<bit-card>Item 3</bit-card>
</bit-grid>
<script type="module">
import '@vielzeug/buildit/grid';
import '@vielzeug/buildit/card';
</script>Column Layouts
Fixed Columns
Create grids with a fixed number of columns from 1 to 12.
Responsive Columns with Breakpoints
Use cols-sm, cols-md, cols-lg, cols-xl, and cols-2xl attributes for explicit responsive control.
Resize to See It Work
Try resizing your browser window or use the viewport controls above to see the grid automatically adapt:
- Mobile: 1 column
- Small (≥640px): 2 columns
- Medium (≥768px): 3 columns
- Large (≥1024px): 4 columns
Breakpoint Reference
Breakpoints respond to the element's own width via ResizeObserver, so they work correctly inside sidebars, modals, ComponentPreviews, and any constrained space.
| Breakpoint | Attribute | Min Element Width | Example |
|---|---|---|---|
| Mobile | cols="1" | Default | 1 column |
| Small | cols-sm="2" | ≥640px | 2 columns |
| Medium | cols-md="3" | ≥768px | 3 columns |
| Large | cols-lg="4" | ≥1024px | 4 columns |
| Extra Large | cols-xl="6" | ≥1280px | 6 columns |
| 2X Large | cols-2xl="8" | ≥1536px | 8 columns |
Row Layouts
Define explicit row counts for dashboard-style layouts.
Gap Sizes
Control spacing between grid items.
Available Gap Sizes
| Size | Token | Value |
|---|---|---|
none | - | 0 |
xs | --size-1 | 0.25rem (4px) |
sm | --size-2 | 0.5rem (8px) |
md | --size-4 | 1rem (16px) |
lg | --size-6 | 1.5rem (24px) |
xl | --size-8 | 2rem (32px) |
2xl | --size-12 | 3rem (48px) |
Flow Control
Control how items flow into the grid.
Row Flow (Default)
Column Flow
Dense Packing
Automatically fill gaps with smaller items that come later.
Dense Packing
With flow="row-dense", item B fills the gap after the first wide item, rather than leaving it empty. This creates a more compact layout but may change visual order.
Alignment
Vertical Alignment (align-items)
Horizontal Alignment (justify-items)
Grid Items
Use bit-grid-item for precise placement and span control within a bit-grid.
Column and Row Spans
col-span and row-span cover the common case of stretching an item across multiple tracks. Use "full" to span all columns or rows.
Explicit Placement
Use the col and row attributes to set raw CSS grid-column / grid-row values. This accepts any valid CSS shorthand: "2 / 5", "span 3", "1 / -1", etc.
Item Alignment
Use align and justify on bit-grid-item to override the grid's default alignment for a single cell.
Named Grid Areas
Use areas (and its breakpoint variants areas-sm, areas-md, areas-lg, areas-xl, areas-2xl) to define named regions on the grid. The active value is resolved from the element's own width via ResizeObserver, identical to how cols-* breakpoints work. Children can be placed into regions with style="grid-area: name" or via the col / row attrs on bit-grid-item.
Basic Areas
Responsive Areas
Provide different area templates at each breakpoint. The grid switches between them as the element resizes — a single-column stack on small widths, full page layout on larger ones.
Responsive Auto-fit Mode
Use responsive to let the grid fit as many columns as possible based on a minimum column width. Set min-col-width to control the threshold (default: 250px).
Auto-fit vs Fixed Columns
Use responsive for fluid layouts where column count depends on available space. Use cols with optional breakpoint attributes (cols-sm, cols-md, etc.) for explicit control.
API Reference
Grid Attributes
| Attribute | Type | Default | Description |
|---|---|---|---|
cols | '1'–'12' | 'auto' | - | Number of columns |
cols-sm | '1'–'12' | 'auto' | - | Columns when element width ≥ 640px |
cols-md | '1'–'12' | 'auto' | - | Columns when element width ≥ 768px |
cols-lg | '1'–'12' | 'auto' | - | Columns when element width ≥ 1024px |
cols-xl | '1'–'12' | 'auto' | - | Columns when element width ≥ 1280px |
cols-2xl | '1'–'12' | 'auto' | - | Columns when element width ≥ 1536px |
rows | '1'–'12' | 'auto' | - | Number of explicit rows |
gap | 'none' | 'xs' | 'sm' | 'md' | 'lg' | 'xl' | '2xl' | 'md' | Gap between items |
align | 'start' | 'center' | 'end' | 'stretch' | 'baseline' | - | align-items for all cells |
justify | 'start' | 'center' | 'end' | 'stretch' | - | justify-items for all cells |
flow | 'row' | 'column' | 'row-dense' | 'column-dense' | 'row' | grid-auto-flow direction |
responsive | boolean | false | Enable auto-fit mode |
min-col-width | string | 250px | Minimum column width in responsive mode |
fullwidth | boolean | false | Stretch the grid to fill its container's width |
areas | string | - | CSS grid-template-areas value |
areas-sm | string | - | grid-template-areas when element width ≥ 640px |
areas-md | string | - | grid-template-areas when element width ≥ 768px |
areas-lg | string | - | grid-template-areas when element width ≥ 1024px |
areas-xl | string | - | grid-template-areas when element width ≥ 1280px |
areas-2xl | string | - | grid-template-areas when element width ≥ 1536px |
Grid Item Attributes
| Attribute | Type | Default | Description |
|---|---|---|---|
col-span | '1'–'12' | 'full' | - | Columns to span; 'full' = 1 / -1 |
row-span | '1'–'6' | 'full' | - | Rows to span; 'full' = 1 / -1 |
col | string | - | Raw grid-column value — overrides col-span |
row | string | - | Raw grid-row value — overrides row-span |
align | 'start' | 'center' | 'end' | 'stretch' | - | align-self for this cell |
justify | 'start' | 'center' | 'end' | 'stretch' | - | justify-self for this cell |
CSS Custom Properties
These are fallback values — attributes take precedence when set.
| Property | Default | Description |
|---|---|---|
--grid-cols | - | Fallback column template |
--grid-rows | - | Fallback row template |
--grid-gap | var(--size-4) | Fallback gap |
--grid-row-gap | var(--grid-gap) | Fallback row gap |
--grid-col-gap | var(--grid-gap) | Fallback column gap |
Examples
Dashboard Layout
Asymmetric Layout
Bento-style Layout with Named Areas
Accessibility
The grid component follows WAI-ARIA best practices.
bit-grid
✅ Semantic Structure
- Maintains semantic HTML structure and document reading order by default.
- Grid layout is purely visual — keyboard navigation follows DOM order.
✅ Screen Readers
- Compatible with screen readers.
- Be mindful of visual vs. DOM order when using
flow="dense"or explicit item placement.
Dense Packing & Accessibility
When using flow="row-dense" or flow="column-dense", items may appear in a different visual order than they exist in the DOM. This can confuse screen reader users and keyboard navigators. Use dense packing only when layout aesthetics outweigh reading order, or restore meaningful order with tabindex.
Best Practices
Do:
- Start mobile-first: set
cols="1"as the base and scale up withcols-sm,cols-md, etc. - Use
responsivemode for content-driven layouts where column count should adjust automatically to available space. - Use
bit-grid-itemwithcol-spanfor featured items that need to span multiple columns. - Use
areaswith named regions for complex or asymmetric page layouts.
Don't:
- Use
flow="dense"when reading order matters — it visually reorders items relative to the DOM. - Mix
responsivewith a fixedcolsattribute; they target different layout modes.