demo D09
Pinned and Frozen Columns, Left and Right in htmx
Left and right pinned regions scrolling independently
layout: { pin: 'start' | 'end' }
This grid holds a chosen set of columns fixed against the left and right edges while the rest scroll underneath them. It keeps the identifying fields, an id or a name, in view no matter how far across a wide table someone scrolls, so a row never loses its label.
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="pinned-columns-table" data-lattice-grid>
<thead>
<tr>
<th data-field="account">Account</th>
<th data-field="service">Service</th>
<th data-field="resource">Resource</th>
<th data-field="region">Region</th>
<th data-field="change">Change</th>
<th data-field="cost">Monthly cost</th>
</tr>
</thead>
<tbody>
<tr data-id="1"><td>Legacy (Do Not Decommission)</td><td>Relational Database</td><td>legacy-nat-delete-after-friday</td><td>us-east-1</td><td>-0.0468</td><td>31612.55</td></tr>
<tr data-id="2"><td>Cost Optimisation Programme</td><td>Elastic Compute</td><td>tmp-jumpbox-03</td><td>us-east-1</td><td>0.1143</td><td>6109.09</td></tr>
<tr data-id="3"><td>Data & Insights</td><td>Block Storage</td><td>test-api-terraform-imported-by-hand</td><td>us-east-1</td><td>0.1082</td><td>42.69</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';
// Account stays at the start and Monthly cost at the end while the rest scroll
// between them, so the label and the figure that matter are always in view.
// 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('pinned-columns-table'), {
selection: 'multiple',
toolPanel: {
side: 'left',
panels: ['columns', 'filters', 'views', 'quick'],
actions: ['undo', 'redo', 'export', 'restore', 'maximise'],
exportName: 'lattice-demo',
},
columns: [
{ field: 'account', title: 'Account', filter: { type: 'set' }, layout: { pin: 'start', width: 200 } },
{ field: 'service', title: 'Service', filter: { type: 'set' } },
{ field: 'resource', title: 'Resource', layout: { flex: 1, min: 200, max: 320 } },
{ field: 'region', title: 'Region', filter: { type: 'set' } },
{ field: 'environment', title: 'Env', filter: { type: 'set' },
cell: { decoration: 'pill', variant: { map: {
prod: 'danger', 'prod-2': 'danger', 'definitely-prod': 'danger',
staging: 'warning', untagged: 'warning', test: 'info',
dev: 'success', 'not-prod': 'neutral',
} } } },
{ field: 'change', title: 'Change', type: 'number',
layout: { width: 140, min: 140 }, format: { style: 'percent', decimals: 1 } },
{ field: 'cost', title: 'Monthly cost', type: 'number', total: 'sum',
layout: { width: 170, pin: 'end' },
format: { style: 'currency', currency: 'USD', decimals: 2 } },
],
});
</script>
Pinning columns to keep key fields in view while the rest scrolls
Pinned columns, also called frozen columns, hold a chosen set of fields fixed against the left or right edge of a JavaScript data grid while the remaining columns scroll independently underneath them. Reach for this whenever a wide table carries an identifying column, such as a name or an order number, that a reader needs alongside whatever metric they have scrolled across to compare, or whenever row-level actions belong on the right edge regardless of horizontal scroll position. Lattice Grid controls this per column through layout: { pin: 'start' | 'end' }, so a column definition declares its own pinned region rather than the grid applying a single global split; a leading identifier column takes 'start' and a trailing actions column takes 'end', with the unpinned columns forming a centre region that scrolls on its own.
The pinned and centre regions render as separate scrolling containers sharing one header and one set of row boundaries, so vertical scrolling stays in sync across all three without a manual reconciliation step. Horizontal scroll position, sort state and column widths in the pinned regions are unaffected by scrolling the centre region, and keyboard navigation moves across the boundary between regions as a single continuous row rather than treating pinned columns as a separate tab stop sequence. Because only the centre region’s off-screen columns are virtualised out of the DOM, adding pinned columns does not change how many rows the grid can hold before scroll performance degrades.
How do you freeze columns in a JavaScript data grid?
Set layout: { pin: 'start' } on a column definition to fix it to the left edge, or pin: 'end' to fix it to the right edge. Lattice Grid renders pinned columns in their own scrolling region alongside the centre columns, keeping them in view while the rest of the row scrolls horizontally. See the step-by-step how to freeze columns guide for a runnable example and the source on GitHub.