Lattice Grid Buy a licence

demo D03

Thirty columns, thirty types in htmx

The realistic wide grid: text, numbers, dates, booleans, all at once

columns: [{ type: … }]

This is the realistic wide record: thirty columns of text, numbers, dates, booleans and the odd nested object side by side, each sorting, filtering and formatting by its own rules. It is closer to a real product screen than a tidy demo, so you can see how the grid copes when every column wants to behave differently.

Building…
Loading a live grid…

This is the htmx version. A server-rendered table is upgraded in place, so the page is readable before any script runs and gains the full grid when the module loads, which suits a progressively enhanced app.

The configuration

<link rel="stylesheet" href="https://cdn.latticegrid.dev/lattice-grid@1.107.0/lattice-grid.min.css">

<!-- A real, server-rendered table: readable with JavaScript off. data-field on
     each header names the column that cell becomes, and data-id on each row is
     the key the grid carries it under. -->
<table id="thirty-columns-table" data-lattice-grid>
  <thead>
    <tr>
      <th data-field="service">Service</th>
      <th data-field="team">Team</th>
      <th data-field="owner">Owner</th>
      <th data-field="region">Region</th>
      <th data-field="active">Active</th>
      <th data-field="deprecated">Deprecated</th>
      <th data-field="version">Version</th>
      <th data-field="instances">Instances</th>
      <th data-field="cpuCores">vCPU</th>
      <th data-field="memoryGb">Memory GB</th>
      <th data-field="diskBytes">Disk</th>
      <th data-field="requests">Requests</th>
      <th data-field="errors">Errors</th>
      <th data-field="errorRate">Error rate</th>
      <th data-field="p50Ms">p50 ms</th>
      <th data-field="p95Ms">p95 ms</th>
      <th data-field="p99Ms">p99 ms</th>
      <th data-field="uptime">Uptime</th>
      <th data-field="monthlyCost">Monthly cost</th>
      <th data-field="budget">Budget</th>
      <th data-field="headcount">Headcount</th>
      <th data-field="openIncidents">Incidents</th>
      <th data-field="lastDeploy">Last deploy</th>
      <th data-field="created">Created</th>
      <th data-field="slaTarget">SLA</th>
      <th data-field="onCall">On call</th>
      <th data-field="repository">Repository</th>
      <th data-field="notes">Notes</th>
    </tr>
  </thead>
  <tbody>
    <tr data-id="1"><td>svc-00001</td><td>Search</td><td>k.yilmaz</td><td>Malmö</td><td>false</td><td>false</td><td>1.2.10</td><td>49</td><td>32</td><td>256</td><td>8412699469830</td><td>3757297</td><td>94112</td><td>0.025</td><td>27</td><td>140</td><td>3906</td><td>0.9523</td><td>37920.55</td><td>20036</td><td>2</td><td>5</td><td>2026-06-21</td><td>2024-06-10</td><td>0.99</td><td>m.svensson</td><td>github.com/example/billing-svc-1</td><td></td></tr>
    <tr data-id="2"><td>svc-00002</td><td>Mobile</td><td>m.svensson</td><td>Leeds</td><td>true</td><td>false</td><td>2.3.6</td><td>162</td><td>20</td><td>104</td><td>1265685622114</td><td>1623478</td><td>34948</td><td>0.0215</td><td>9</td><td>427</td><td>1281</td><td>0.9871</td><td>30070.04</td><td>56591.85</td><td>2</td><td>2</td><td>2025-10-09</td><td>2022-01-06</td><td>0.99</td><td>m.svensson</td><td>github.com/example/billing-svc-2</td><td></td></tr>
    <tr data-id="3"><td>svc-00003</td><td>Identity</td><td>j.okafor</td><td>Zürich</td><td>true</td><td>false</td><td>1.11.1</td><td>20</td><td>17</td><td>212</td><td>8645523667568</td><td>3983580</td><td>66969</td><td>0.0168</td><td>57</td><td>476</td><td>2637</td><td>0.9895</td><td>36278.48</td><td>8543.17</td><td>1</td><td>2</td><td>2026-08-16</td><td>2024-10-16</td><td>0.99</td><td>j.okafor</td><td>github.com/example/search-svc-3</td><td></td></tr>
    <!-- the rest of the rows, rendered by your server -->
  </tbody>
</table>

<script type="module">
  import { hydrateTable } from 'https://cdn.latticegrid.dev/lattice-grid@1.107.0/modules/htmx.esm.min.js';

  // The markup carries the rows; a server-rendered table's column types are
  // declared in the config hydrateTable takes. The grid replaces the table
  // where it stood.
  const grid = hydrateTable(document.getElementById('thirty-columns-table'), {
    selection: 'multiple',
    toolPanel: {
      side: 'left',
      panels: ['columns', 'filters', 'views', 'quick'],
      actions: ['undo', 'redo', 'export', 'restore', 'maximise'],
      exportName: 'services',
    },
    columns: [
      { field: 'service', title: 'Service', layout: { pin: 'start', width: 140 } },
      { field: 'team', title: 'Team', filter: { type: 'set' } },
      { field: 'owner', title: 'Owner', filter: { type: 'set' } },
      { field: 'tier', title: 'Tier',
        cell: { decoration: 'pill', variant: { map: { gold: 'warning', silver: 'neutral', bronze: 'info' } } } },
      { field: 'state', title: 'State',
        cell: { decoration: 'dot', variant: { map: { healthy: 'success', degraded: 'warning', failing: 'danger', unknown: 'neutral' } } } },
      { field: 'region', title: 'Region', filter: { type: 'set' } },
      { field: 'active', title: 'Active', type: 'boolean' },
      { field: 'deprecated', title: 'Deprecated', type: 'boolean' },
      { field: 'version', title: 'Version', layout: { width: 100 } },
      { field: 'instances', title: 'Instances', type: 'number' },
      { field: 'cpuCores', title: 'vCPU', type: 'number' },
      { field: 'memoryGb', title: 'Memory GB', type: 'number' },
      { field: 'diskBytes', title: 'Disk', type: 'number', format: { notation: 'compact' } },
      { field: 'requests', title: 'Requests', type: 'number', format: { notation: 'compact' } },
      { field: 'errors', title: 'Errors', type: 'number' },
      { field: 'errorRate', title: 'Error rate', type: 'number', format: { style: 'percent', decimals: 2 } },
      { field: 'p50Ms', title: 'p50 ms', type: 'number' },
      { field: 'p95Ms', title: 'p95 ms', type: 'number' },
      { field: 'p99Ms', title: 'p99 ms', type: 'number' },
      { field: 'uptime', title: 'Uptime', type: 'number', format: { style: 'percent', decimals: 3 } },
      { field: 'monthlyCost', title: 'Monthly cost', type: 'number', format: { style: 'currency', currency: 'USD' }, total: 'sum' },
      { field: 'budget', title: 'Budget', type: 'number', format: { style: 'currency', currency: 'USD' }, total: 'sum' },
      { field: 'headcount', title: 'Headcount', type: 'number', total: 'sum' },
      { field: 'openIncidents', title: 'Incidents', type: 'number', total: 'sum' },
      { field: 'lastDeploy', title: 'Last deploy', type: 'dateString' },
      { field: 'created', title: 'Created', type: 'dateString' },
      { field: 'slaTarget', title: 'SLA', type: 'number', format: { style: 'percent', decimals: 2 } },
      { field: 'onCall', title: 'On call' },
      { field: 'repository', title: 'Repository', layout: { width: 260 } },
      { field: 'notes', title: 'Notes' },
    ],
  });
</script>

Configuring thirty columns of mixed types in one JavaScript data grid

Most production grids are not ten narrow text columns; they are a wide record with text, numbers, dates, booleans and the odd nested object side by side, each needing its own comparator, formatter and editor. This demo declares thirty such columns through the columns array, where each entry is an object with a type (columns: [{ type: … }]), so a date column sorts as a date and a boolean renders as a checkbox without extra wiring. Reach for this pattern whenever a dataset arrives already typed, rather than as a flat table of strings, and the grid needs to carry that typing through sorting, filtering and export rather than re-deriving it at render time.

Only the columns inside the visible horizontal band are mounted as DOM cells; the rest exist as data, not markup, so scrolling sideways across thirty columns does not mean thirty columns of live layout work at every frame. Keyboard navigation moves cell to cell across the full set with a single tab stop into the grid, and each header carries the row and column indices a screen reader needs to announce position correctly. A grid at thirty columns behaves the same as one at three, because the type information is declared once, per column, rather than inferred or hand-formatted per cell.

How do you configure column types in a JavaScript data grid?

Each entry in the columns array takes a type, such as 'text', 'number', 'date' or 'boolean', and the grid picks the matching comparator, formatter and default editor for that type. Shared behaviour across several columns is set once in columnDefaults rather than repeated on every entry, which keeps a thirty-column configuration readable and easy to audit for consistency.