Skip to content

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 areas to define named regions directly on the grid
  • 🔄 Flow Control: Row, column, and dense packing modes
  • 📊 7 Gap Sizes: From none to 2xl with separate row/column gap support
  • 🧲 Alignment Control: Align and justify items with CSS Grid properties
  • 🎨 Grid Item Component: Precise placement with bit-grid-item using spans or raw CSS grid shorthand
  • 🔧 Customizable: CSS custom properties available as fallbacks

Source Code

View Source Code (Grid)
ts
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)
ts
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

html
<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.

PreviewCode
RTL

Responsive Columns with Breakpoints

Use cols-sm, cols-md, cols-lg, cols-xl, and cols-2xl attributes for explicit responsive control.

PreviewCode
RTL

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.

BreakpointAttributeMin Element WidthExample
Mobilecols="1"Default1 column
Smallcols-sm="2"≥640px2 columns
Mediumcols-md="3"≥768px3 columns
Largecols-lg="4"≥1024px4 columns
Extra Largecols-xl="6"≥1280px6 columns
2X Largecols-2xl="8"≥1536px8 columns

Row Layouts

Define explicit row counts for dashboard-style layouts.

PreviewCode
RTL

Gap Sizes

Control spacing between grid items.

PreviewCode
RTL

Available Gap Sizes

SizeTokenValue
none-0
xs--size-10.25rem (4px)
sm--size-20.5rem (8px)
md--size-41rem (16px)
lg--size-61.5rem (24px)
xl--size-82rem (32px)
2xl--size-123rem (48px)

Flow Control

Control how items flow into the grid.

Row Flow (Default)

PreviewCode
RTL

Column Flow

PreviewCode
RTL

Dense Packing

Automatically fill gaps with smaller items that come later.

PreviewCode
RTL

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)

PreviewCode
RTL

Horizontal Alignment (justify-items)

PreviewCode
RTL

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.

PreviewCode
RTL

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.

PreviewCode
RTL

Item Alignment

Use align and justify on bit-grid-item to override the grid's default alignment for a single cell.

PreviewCode
RTL

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

PreviewCode
RTL

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.

PreviewCode
RTL

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).

PreviewCode
RTL

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

AttributeTypeDefaultDescription
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
responsivebooleanfalseEnable auto-fit mode
min-col-widthstring250pxMinimum column width in responsive mode
fullwidthbooleanfalseStretch the grid to fill its container's width
areasstring-CSS grid-template-areas value
areas-smstring-grid-template-areas when element width ≥ 640px
areas-mdstring-grid-template-areas when element width ≥ 768px
areas-lgstring-grid-template-areas when element width ≥ 1024px
areas-xlstring-grid-template-areas when element width ≥ 1280px
areas-2xlstring-grid-template-areas when element width ≥ 1536px

Grid Item Attributes

AttributeTypeDefaultDescription
col-span'1'–'12' | 'full'-Columns to span; 'full' = 1 / -1
row-span'1'–'6' | 'full'-Rows to span; 'full' = 1 / -1
colstring-Raw grid-column value — overrides col-span
rowstring-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.

PropertyDefaultDescription
--grid-cols-Fallback column template
--grid-rows-Fallback row template
--grid-gapvar(--size-4)Fallback gap
--grid-row-gapvar(--grid-gap)Fallback row gap
--grid-col-gapvar(--grid-gap)Fallback column gap

Examples

Dashboard Layout

PreviewCode
RTL

Asymmetric Layout

PreviewCode
RTL

Bento-style Layout with Named Areas

PreviewCode
RTL

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 with cols-sm, cols-md, etc.
  • Use responsive mode for content-driven layouts where column count should adjust automatically to available space.
  • Use bit-grid-item with col-span for featured items that need to span multiple columns.
  • Use areas with 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 responsive with a fixed cols attribute; they target different layout modes.