title: Custom rendering with usePretableModel description: Build your own grid UI on top of the engine — full control over cell rendering, sort UI, filter UI, selection, focus, and pinned columns.

Custom rendering with usePretableModel

usePretableModel returns the engine's full state: the grid model (with interaction methods), the snapshot (sort, filters, selection, focus), the render snapshot (which rows to render at what positions), and telemetry. You write your own JSX on top, applying the [data-pretable-*] attribute contract so @pretable/ui/grid.css styles the result.

This page walks through a minimal-but-complete example.

When to use it

If none of these apply, the <Pretable> drop-in is simpler.

The hook

import { usePretableModel } from "@pretable/react";

const { grid, snapshot, renderSnapshot, telemetry } = usePretableModel({
  columns,
  rows,
  viewportHeight,
  // optional:
  viewportWidth,
  overscan,
  interactionOverrides,
  measuredHeights,
});

What you get back:

See API reference for the full type signatures.

A minimal working example

import { useResolvedHeights, usePretableModel } from "@pretable/react";
import type { PretableColumn, PretableRow } from "@pretable/react";

interface Person extends PretableRow {
  id: string;
  name: string;
  role: string;
  city: string;
}

const columns: PretableColumn<Person>[] = [
  { id: "name", header: "Name", getValue: (r) => r.name, widthPx: 200 },
  { id: "role", header: "Role", getValue: (r) => r.role, widthPx: 200 },
  { id: "city", header: "City", getValue: (r) => r.city, widthPx: 160 },
];

export function MyGrid({ rows }: { rows: Person[] }) {
  const { headerHeight } = useResolvedHeights();
  const viewportHeight = 480;

  const { grid, snapshot, renderSnapshot } = usePretableModel({
    columns,
    rows,
    viewportHeight,
  });

  const sortedColumn = snapshot.sort.columnId;
  const sortedDirection = snapshot.sort.direction;

  return (
    <div
      data-pretable-scroll-viewport=""
      style={{
        height: viewportHeight,
        overflow: "auto",
        position: "relative",
      }}
      onScroll={(e) => {
        const el = e.currentTarget;
        grid.setViewport({
          scrollTop: el.scrollTop,
          scrollLeft: el.scrollLeft,
          height: viewportHeight,
          width: el.clientWidth,
        });
      }}
    >
      {/* Header row */}
      <div
        data-pretable-header-row=""
        style={{
          position: "sticky",
          top: 0,
          zIndex: 3,
          display: "flex",
          height: headerHeight,
          minWidth: renderSnapshot.totalWidth,
        }}
      >
        {renderSnapshot.columns.map((col) => {
          const isSorted = sortedColumn === col.id;
          const next =
            isSorted && sortedDirection === "asc"
              ? "desc"
              : isSorted && sortedDirection === "desc"
                ? null
                : "asc";

          return (
            <button
              key={col.id}
              data-pretable-header-cell=""
              data-pinned={col.pinned === "left" ? "left" : undefined}
              onClick={() => grid.setSort(col.id, next)}
              style={{
                position: "absolute",
                left: col.left,
                width: col.width,
                top: 0,
                height: "100%",
                border: 0,
                background: "transparent",
                textAlign: "left",
              }}
            >
              {columns.find((c) => c.id === col.id)?.header}
              {isSorted ? (sortedDirection === "asc" ? " ▲" : " ▼") : ""}
            </button>
          );
        })}
      </div>

      {/* Body */}
      <div
        data-pretable-scroll-content=""
        style={{
          position: "relative",
          height: renderSnapshot.totalHeight,
          minWidth: renderSnapshot.totalWidth,
        }}
      >
        {renderSnapshot.rows.map((row) => {
          const isSelected = snapshot.selection.rowIds.includes(row.id);
          const isFocused = snapshot.focus.rowId === row.id;

          return (
            <div
              key={row.id}
              data-pretable-row=""
              style={{
                position: "absolute",
                top: row.top,
                height: row.height,
                left: 0,
                right: 0,
                display: "flex",
              }}
              onClick={() => {
                grid.selectRow(row.id);
                grid.setFocus(row.id, columns[0]?.id ?? null);
              }}
            >
              {renderSnapshot.columns.map((col) => {
                const column = columns.find((c) => c.id === col.id);
                const value = column?.getValue?.(row.row) ?? "";

                return (
                  <div
                    key={col.id}
                    data-pretable-cell=""
                    data-pinned={col.pinned === "left" ? "left" : undefined}
                    data-selected={isSelected ? "true" : "false"}
                    data-focused={
                      isFocused && snapshot.focus.columnId === col.id
                        ? "true"
                        : "false"
                    }
                    style={{
                      position: "absolute",
                      left: col.left,
                      width: col.width,
                      height: "100%",
                      boxSizing: "border-box",
                    }}
                  >
                    {String(value)}
                  </div>
                );
              })}
            </div>
          );
        })}
      </div>
    </div>
  );
}

This renders a working sortable grid with click-to-select. Wire @pretable/ui/themes/excel.css + @pretable/ui/grid.css and the data attributes get styled automatically — gridlines, header bg, selection background, sort indicator color.

What this example does NOT cover

The minimal example above is a starting point. For production grids, you'll likely also need:

For the full reference implementation, see packages/react-surface/src/pretable-surface.tsx in the repository — that's what the bench's pretable adapter and the website's playground use internally. It's marked private (lives at @pretable-internal/react-surface), but reading its source is the canonical reference for the patterns above.

Where to go next