Grid Custom rendering

Custom rendering

Customize cells, headers, and rows while retaining Pretable's indexed renderer.

Prefer <PretableSurface> render hooks when you need custom markup. They preserve windowing, variable-height layout, keyboard behavior, selection, and accessibility. renderBodyCell and renderHeaderCell, below, pick presentation for every column from one place — the shape a design-system wrapper reaches for:

Grid-level cell and header renderers

A support-ticket grid where renderBodyCell and renderHeaderCell on PretableSurface itself pick the presentation per column, the pattern a design-system wrapper reaches for instead of per-column render hooks.

.md

When to use it

  • You need real sort, filter, grouping, and selection UI in your app.
  • Different columns need different React presentations.
  • You are wrapping the surface for a design system, the way the example above does.
  • You need pinned columns rendered as sticky elements.

Cell and header renderers

The general shape behind the example above:

tsx
<PretableSurface
  rows={rows}
  columns={columns}
  getRowId={(row) => row.id}
  renderBodyCell={({ row, column, value, formattedValue }) =>
    column.id === "status" ? <StatusPill status={row.status} /> : formattedValue
  }
  renderHeaderCell={({ column }) => <strong>{column.header}</strong>}
/>

Column-level render and renderEditor callbacks are useful when presentation belongs with one typed column — see the render example on Cell renderers. Surface-level hooks, shown above, are better for a design-system wrapper: one function covers every column, and column.render still wins per-column when both are present. Shared presentations such as PretableBadge, PretableDelta, PretableEntity, and PretableStatus keep semantic color, contrast, and secondary text aligned with the active theme regardless of which hook renders them. See Cell presentations.

Props and data attributes

Use getBodyCellProps, getHeaderCellProps, and getRowProps to add event handlers or ARIA metadata without rebuilding the surface. Styling hooks include:

  • data-pretable-scroll-viewport
  • data-pretable-header-row and data-pretable-header-cell
  • data-pretable-row and data-pretable-cell
  • data-pretable-group-row
  • data-pretable-selected and data-pretable-focused
  • data-pretable-pinned

When sizing custom chrome, use getDensityHeights() instead of hardcoded header and row heights. It reads the active density tokens synchronously; see Density helpers for server rendering and runtime-density subscriptions.

Query and interaction control

Rows mode keeps data declarative. The exact query/onQueryChange pair controls filters, sort, and grouping together. state remains limited to UI interaction and column-layout slices; onSelectionChange and onFocusChange publish those interactions.

For an imperative producer, create a local row model and pass model={rowModel}. Query and row mutations go to the row model; the surface's grid handle remains UI-only.

Fully custom renderer

If you need canvas, another framework, or markup that cannot use the surface, use the headless engine directly — no <PretableSurface> in sight, just the row model, useSyncExternalStore, and a snapshot range you render yourself:

Headless custom renderer

Drive your own markup from the @pretable/core row model with useSyncExternalStore — no grid renderer involved.

.md

The shape in general:

ts
const rowModel = createLocalRowModel({ rows, columns });
const grid = createGrid({ rowModel, columns }); // only if UI state is needed
const snapshot = rowModel.getState().snapshot;
const window = snapshot.range(start, end);

Never enumerate the complete model merely to draw one viewport.

See also