Grid Column Layout

Column Layout

Resize, reorder, pin, and autosize columns; per-column min/max; controlled state.

Pretable supports column resize, reorder, pin, and autosize out of the box. Width and order live behind narrow controlled-state slices that mirror the established pattern (state.sort, state.selection, etc.).

Resize

Every header where column.resizable !== false exposes a 4px hit-target on its right edge. Pointer-down on the handle starts a drag; the engine commits the new width on pointer-up. onColumnWidthsChange fires once per drag-end with the full widths map for all data columns — there is no per-frame chatter during the drag.

Per-column minWidthPx and maxWidthPx clamp the result. Engine-wide defaults are 40px min and 800px max; supply tighter bounds on the column when the data has known constraints.

Double-clicking the resize handle calls grid.autosizeColumn(columnId) — the keyboard-free shortcut for "fit this column to its content."

The synthetic row-select column has no resize handle. Set resizable: false on any other column to opt it out.

Reorder

Pointer-down on the header itself (anywhere except the resize handle) starts a reorder gesture. A 5px movement threshold disambiguates from sort clicks — short drags still trigger sort, longer drags trigger reorder.

While dragging, a ghost element follows the cursor and a 2px drop indicator snaps to the nearest column boundary. onColumnOrderChange fires once on drag-end with the new id order.

Cross-boundary auto-pin. Dragging a column into the leftmost pinned region pins it there; dragging a pinned column out unpins it. When pin state changes alongside the reorder, onColumnPinnedChange fires alongside onColumnOrderChange in the same commit.

Per-column reorderable: false opts out. The synthetic row-select column is never reorderable.

Pin

Programmatic pinning is grid.setColumnPinned(columnId, "left" | null). Setting a pin repositions the column to the pin-region boundary; unpinning leaves it at the current boundary position.

The synthetic row-select column is always at position 0; it is never pinnable, reorderable, or resizable through the gesture or programmatic APIs.

Autosize

  • grid.autosizeColumn(columnId, options?) — fit one column to its measured content width.
  • grid.autosizeColumns(options?) — fit every column.
  • Double-click on a column's resize handle is the keyboard-free shortcut for the single-column form.

AutosizeOptions controls the algorithm (sample size, padding, header inclusion). Defaults are sensible; reach for the options when you need to constrain measurement on very long datasets.

Reset

grid.resetColumnLayout() restores order, widths, and pinned state to the original columns prop snapshot taken at mount. Useful for "Reset layout" toolbar buttons that recover after a user has rearranged the grid.

Controlled state

Three independently-controlled slices on state:

  • columnWidths: Record<string, number>
  • columnOrder: readonly string[]
  • columnPinned: Record<string, "left" | null>

Three matching callbacks:

  • onColumnWidthsChange
  • onColumnOrderChange
  • onColumnPinnedChange

Drag-end-only emission. Programmatic mutations from controlled-prop reapply do not fire callbacks; only user-initiated commits do. This matches the established pattern from sub-project B (selection, focus, sort) — controlled props are a one-way push from consumer to engine, callbacks are a one-way push from user gesture to consumer.

Each slice is independent: pass the slices you want to own, omit the rest. The engine still owns viewport, virtualization, and any uncontrolled slices.

Code example

tsx
import { useState } from "react"; import { PretableSurface, type PretableColumn } from "@pretable/react"; interface Row extends Record<string, unknown> { id: string; name: string; city: string; } const columns: PretableColumn<Row>[] = [ { id: "name", header: "Name", widthPx: 200, minWidthPx: 80 }, { id: "city", header: "City", widthPx: 160 }, ]; export function ResizableGrid({ rows }: { rows: Row[] }) { const [columnWidths, setColumnWidths] = useState<Record<string, number>>({}); return ( <PretableSurface<Row> ariaLabel="Resizable grid" columns={columns} rows={rows} getRowId={(row) => row.id} state={{ columnWidths }} onColumnWidthsChange={setColumnWidths} viewportHeight={420} /> ); }

The same pattern extends to columnOrder and columnPinned — pair each slice with its useState and the matching onColumn*Change callback.

See also

  • Selection — cell-range model, controlled state for the selection slice.
  • Keyboard — full keyboard contract.
  • Clipboard — Cmd/Ctrl+C TSV defaults and overrides.
  • API reference — complete type signatures.