Skip to main content
The Grid widget provides a powerful 2D layout system with explicit placement, spans, and auto-placement for complex UI structures.

Basic Usage

import { ui } from "@rezi-ui/core";

ui.grid({ columns: 3 }, [
  ui.text("Item 1"),
  ui.text("Item 2"),
  ui.text("Item 3"),
  ui.text("Item 4"),
  ui.text("Item 5"),
  ui.text("Item 6"),
]);

Props

columns
number | string
required
Column definition. Either:
  • number - Fixed number of equal-width columns
  • string - Track sizes (e.g., "1fr 2fr 1fr", "auto 20 auto")
rows
number | string
Row definition. Either:
  • number - Fixed number of equal-height rows
  • string - Track sizes (e.g., "auto 10 1fr")
gap
number
default:"0"
Spacing between all cells (columns and rows).
rowGap
number
Spacing between rows. Overrides gap for rows.
columnGap
number
Spacing between columns. Overrides gap for columns.
transition
TransitionSpec
Declarative transition settings for container animations.
exitTransition
TransitionSpec
Exit transition before unmount.

Column/Row Definitions

Fixed Columns

ui.grid({ columns: 3 }, [
  ui.text("A"),
  ui.text("B"),
  ui.text("C"),
  ui.text("D"),
  ui.text("E"),
  ui.text("F"),
]);
Creates a 3-column grid with equal-width columns.

Fractional Units (fr)

Distribute remaining space proportionally:
ui.grid({ columns: "1fr 2fr 1fr" }, [
  ui.text("Narrow"),  // 25% width
  ui.text("Wide"),    // 50% width
  ui.text("Narrow"),  // 25% width
]);

Fixed Sizes

Use absolute cell counts:
ui.grid({ columns: "20 1fr 20" }, [
  ui.text("Left"),    // 20 cells wide
  ui.text("Center"),  // Fills remaining
  ui.text("Right"),   // 20 cells wide
]);

Auto Sizing

Size to content:
ui.grid({ columns: "auto 1fr auto" }, [
  ui.text("Fit"),
  ui.text("Flexible"),
  ui.text("Fit"),
]);

Mixed Definitions

ui.grid({ columns: "auto 30 1fr 2fr 15" }, items);

Explicit Placement

Place items at specific grid positions:
ui.grid({ columns: 4, rows: 3 }, [
  ui.box({ gridColumn: 1, gridRow: 1 }, [ui.text("Top-left")]),
  ui.box({ gridColumn: 4, gridRow: 1 }, [ui.text("Top-right")]),
  ui.box({ gridColumn: 2, gridRow: 2, colSpan: 2 }, [ui.text("Center span")]),
  ui.box({ gridColumn: 1, gridRow: 3 }, [ui.text("Bottom-left")]),
]);

Placement Props

These props are available on child widgets:
  • gridColumn: number - Starting column (1-based)
  • gridRow: number - Starting row (1-based)
  • colSpan: number - Number of columns to span
  • rowSpan: number - Number of rows to span

Spans

Column Spans

ui.grid({ columns: 4 }, [
  ui.box({ colSpan: 2 }, [ui.text("Wide")]),
  ui.text("A"),
  ui.text("B"),
  ui.box({ colSpan: 3 }, [ui.text("Extra wide")]),
  ui.text("C"),
]);

Row Spans

ui.grid({ columns: 3, rows: 3 }, [
  ui.box({ gridColumn: 1, gridRow: 1, rowSpan: 3 }, [ui.text("Tall")]),
  ui.text("A"),
  ui.text("B"),
  ui.text("C"),
  ui.text("D"),
  ui.text("E"),
  ui.text("F"),
]);

Multi-Directional Spans

ui.grid({ columns: 4, rows: 4 }, [
  ui.box({ gridColumn: 2, gridRow: 2, colSpan: 2, rowSpan: 2 }, [
    ui.text("Center block"),
  ]),
  // Other items auto-placed around the span
]);

Auto-Placement

Items without explicit placement are automatically positioned:
ui.grid({ columns: 3 }, [
  ui.box({ gridColumn: 2 }, [ui.text("Fixed at column 2")]),
  ui.text("Auto"), // Placed at column 1
  ui.text("Auto"), // Placed at column 3
  ui.text("Auto"), // Placed at column 1, row 2
]);
Auto-placement respects occupied cells from explicit placement and spans.

Gap Spacing

Uniform Gap

ui.grid({ columns: 3, gap: 1 }, items);

Separate Row/Column Gaps

ui.grid({
  columns: 3,
  rowGap: 2,     // 2 cells between rows
  columnGap: 1,  // 1 cell between columns
}, items);

Examples

Dashboard Layout

function dashboard(): VNode {
  return ui.grid(
    {
      columns: "1fr 2fr 1fr",
      rows: "auto 1fr auto",
      gap: 1,
    },
    [
      // Header spans all columns
      ui.box({ colSpan: 3, border: "single", p: 1 }, [
        ui.text("Dashboard", { variant: "heading" }),
      ]),
      
      // Sidebar
      ui.box({ rowSpan: 1, border: "single", p: 1 }, [
        ui.text("Sidebar"),
      ]),
      
      // Main content
      ui.box({ border: "single", p: 1 }, [
        ui.text("Main Content"),
      ]),
      
      // Right panel
      ui.box({ border: "single", p: 1 }, [
        ui.text("Metrics"),
      ]),
      
      // Footer spans all columns
      ui.box({ colSpan: 3, border: "single", p: 1 }, [
        ui.text("Footer"),
      ]),
    ]
  );
}

Card Grid

function cardGrid(items: Array<{ title: string; content: string }>): VNode {
  return ui.grid(
    {
      columns: 4,
      gap: 2,
    },
    items.map((item, i) =>
      ui.box({ border: "rounded", p: 1, key: String(i) }, [
        ui.text(item.title, { style: { bold: true } }),
        ui.text(item.content, { style: { dim: true } }),
      ])
    )
  );
}

Asymmetric Layout

function asymmetricLayout(): VNode {
  return ui.grid(
    {
      columns: "1fr 1fr 1fr 1fr",
      rows: "auto auto",
      gap: 1,
    },
    [
      // Featured item (2x2)
      ui.box({ colSpan: 2, rowSpan: 2, border: "single", p: 1 }, [
        ui.text("Featured", { variant: "heading" }),
      ]),
      
      // Small items
      ui.box({ border: "single", p: 1 }, [ui.text("Item 1")]),
      ui.box({ border: "single", p: 1 }, [ui.text("Item 2")]),
      ui.box({ border: "single", p: 1 }, [ui.text("Item 3")]),
      ui.box({ border: "single", p: 1 }, [ui.text("Item 4")]),
    ]
  );
}

Form Layout

function formGrid(fields: Array<{
  id: string;
  label: string;
  value: string;
  fullWidth?: boolean;
}>): VNode {
  return ui.grid(
    {
      columns: "auto 1fr",
      rowGap: 1,
      columnGap: 2,
    },
    fields.flatMap((field) => {
      const input = ui.input({ id: field.id, value: field.value });
      
      if (field.fullWidth) {
        return [
          ui.text(field.label, { style: { bold: true } }),
          ui.box({ colSpan: 1 }, [input]),
        ];
      }
      
      return [
        ui.text(field.label, { style: { bold: true } }),
        input,
      ];
    })
  );
}
function galleryWithHero(images: Array<{ id: string; src: Uint8Array }>): VNode {
  const [hero, ...rest] = images;
  
  return ui.grid(
    {
      columns: 4,
      rows: "2fr 1fr 1fr",
      gap: 1,
    },
    [
      // Hero image spans 4 columns, 2 rows
      hero && ui.box({ colSpan: 4, rowSpan: 2 }, [
        ui.image({
          id: hero.id,
          src: hero.src,
          width: 80,
          height: 40,
          fit: "cover",
        }),
      ]),
      
      // Thumbnails auto-placed
      ...rest.slice(0, 8).map((img) =>
        ui.image({
          id: img.id,
          src: img.src,
          width: 20,
          height: 10,
          fit: "cover",
          key: img.id,
        })
      ),
    ]
  );
}

Metrics Grid

function metricsGrid(metrics: Array<{
  label: string;
  value: number;
  trend: number[];
}>): VNode {
  return ui.grid(
    {
      columns: "repeat(3, 1fr)",
      gap: 2,
    },
    metrics.map((m) =>
      ui.box({ border: "rounded", p: 1, key: m.label }, [
        ui.text(m.label, { style: { dim: true } }),
        ui.text(String(m.value), { variant: "heading" }),
        ui.sparkline(m.trend, { width: 20 }),
      ])
    )
  );
}
function sidebarLayout(sidebar: VNode, main: VNode): VNode {
  return ui.grid(
    {
      columns: "20 1fr",
      rows: "1fr",
      gap: 1,
    },
    [sidebar, main]
  );
}

Mosaic Grid

function mosaicGrid(): VNode {
  return ui.grid(
    {
      columns: 6,
      rows: 4,
      gap: 1,
    },
    [
      // Large block
      ui.box({ gridColumn: 1, gridRow: 1, colSpan: 3, rowSpan: 2, border: "single" }, [
        ui.text("Large"),
      ]),
      
      // Medium blocks
      ui.box({ gridColumn: 4, gridRow: 1, colSpan: 3, rowSpan: 2, border: "single" }, [
        ui.text("Medium"),
      ]),
      
      // Small blocks
      ui.box({ gridColumn: 1, gridRow: 3, colSpan: 2, rowSpan: 2, border: "single" }, [
        ui.text("Small"),
      ]),
      ui.box({ gridColumn: 3, gridRow: 3, colSpan: 2, rowSpan: 2, border: "single" }, [
        ui.text("Small"),
      ]),
      ui.box({ gridColumn: 5, gridRow: 3, colSpan: 2, rowSpan: 2, border: "single" }, [
        ui.text("Small"),
      ]),
    ]
  );
}

Track Sizing Reference

SyntaxBehaviorExample
N (number)Fixed cell count20 = 20 cells
NfrFractional unit1fr = 1 share, 2fr = 2 shares
autoSize to contentauto
Space-separatedMultiple tracks"1fr 2fr 1fr"

Occupancy-Aware Auto-Placement

The grid layout engine tracks occupied cells and auto-places items in the first available slot:
ui.grid({ columns: 3 }, [
  // Explicit placement reserves cells
  ui.box({ gridColumn: 2, colSpan: 2 }, [ui.text("Wide")]),
  
  // Auto-placed items avoid occupied cells
  ui.text("A"), // Column 1, row 1
  ui.text("B"), // Column 1, row 2 (columns 2-3 occupied)
  ui.text("C"), // Column 2, row 2
]);

Responsive Patterns

While Rezi doesn’t have media queries, you can adapt grid layouts based on terminal size:
import { defineWidget, useTerminalSize } from "@rezi-ui/core";

const ResponsiveGrid = defineWidget<{ items: VNode[] }>((ctx, props) => {
  const size = useTerminalSize();
  
  const columns = size.cols > 120 ? 4 : size.cols > 80 ? 3 : 2;
  
  return ui.grid({ columns, gap: 1 }, props.items);
});

Performance

  • Layout computation is O(n) where n = number of items
  • Explicit placement is instant
  • Auto-placement tracks occupancy for correct positioning

See Also

  • Row - Horizontal stack layout
  • Column - Vertical stack layout
  • Box - Container with border/padding
  • Split Pane - Resizable panels

Build docs developers (and LLMs) love