react-datatable
Customization

Custom React cells

Custom React cells are the main customization tool for changing how a field renders without changing how the table works.

Use them after the column contract is already clear.

Media placeholder: Custom cell examples in a live table

Show one realistic table with a linked company cell, status badge, owner/avatar cell, and row action menu visible together.

Custom cells sit on top of the column contract

A custom cell extends the column definition.

The column still decides the durable behavior:

  • the stable column ID
  • the underlying value source
  • filter and sort behavior
  • grouping support
  • saved-state compatibility

The custom cell decides how that field should appear in the product UI.

That distinction keeps rendering and behavior predictable.

Reach for a custom cell when plain text stops being enough

Custom cells are a good fit when the product needs a field to:

  • combine several row fields into one surface
  • link to a detail page
  • show badges, icons, avatars, or status treatments
  • expose a small inline action
  • reflect workflow meaning instead of only raw stored values

If the column behavior is wrong, fix the column contract first. If the behavior is right but the UI is too generic, a custom cell is usually the right layer.

The renderer receives row and value context

DatatableColumn supports a cell renderer using the TanStack-style cell context.

import type { DatatableColumn } from "./react-datatable"

type Customer = {
  id: string
  company: string
  owner: string
  tier: "free" | "pro" | "enterprise"
}

const columns: DatatableColumn<Customer>[] = [
  {
    id: "company",
    accessorKey: "company",
    header: "Company",
    filterType: "text",
    cell: ({ row, getValue }) => (
      <a href={"/customers/" + row.original.id}>
        {String(getValue())}
      </a>
    ),
  },
]

For simple formatting, getValue() is often enough. For richer domain UI, row.original is usually the real source of truth.

Use row.original for multi-field product UI

Many useful cells render small row summaries instead of a single field value.

cell: ({ row }) => (
  <div className="min-w-0">
    <div className="font-medium">{row.original.company}</div>
    <div className="text-muted-foreground text-sm">{row.original.owner}</div>
  </div>
)

This is the normal pattern when one visual cell needs several row attributes.

The important part is to keep the behavioral column identity stable even when the rendered UI becomes richer.

Keep expensive work outside the renderer

Cells run in the hottest rendering path of the table.

That means the best custom cells are visually rich but computationally boring.

Good pattern:

const money = new Intl.NumberFormat("en", {
  style: "currency",
  currency: "USD",
  maximumFractionDigits: 0,
})

cell: ({ row }) => money.format(row.original.arr)

Less healthy pattern:

  • creating formatters on every render
  • doing heavy synchronous transforms inside the cell
  • deriving large view models repeatedly during scroll
  • making network calls from cells

A cell should usually render quickly from data that is already available.

Interactive cells need to cooperate with the table

Buttons, menus, toggles, and links are valid reasons to use custom cells, but they need extra care.

cell: ({ row }) => (
  <button aria-label={"Open actions for " + row.original.company}>
    Actions
  </button>
)

When a cell becomes interactive, check that it still coexists cleanly with:

  • row selection
  • keyboard navigation
  • row preview/open-row actions
  • focus order
  • accessibility labeling

A good custom cell adds product interaction without making the grid feel unpredictable.

Not every rich row detail belongs in a cell

Custom cells are great for compact, high-signal UI.

They are usually the wrong place for:

  • long descriptions
  • activity feeds
  • complex forms
  • large stacks of actions
  • detail views that need sustained attention

Those heavier experiences generally belong in row preview surfaces or detail routes.

Preserve predictable row height when possible

The table uses virtualization, so custom cells should avoid wildly unstable vertical layout.

A cell can be visually expressive without becoming layout-chaotic.

Prefer:

  • compact stacked text
  • known-size icons, badges, and avatars
  • concise action surfaces
  • predictable wrapping rules

Be careful with:

  • unbounded text blocks
  • async content that changes height dramatically after render
  • large controls that make one row much taller than its neighbors

Keep customization local to presentation

A helpful principle is:

  • use the column contract to define behavior
  • use the custom cell to define presentation
  • use row presentation hooks to define state-aware styling

That separation keeps a custom cell from becoming a hidden place where filtering rules, row state, and styling logic all pile up together.

Accessibility checklist for custom cells

Before you call a custom cell done, check that:

  • links and buttons have clear text or aria-label
  • icon-only controls are still understandable
  • focusable elements can be reached by keyboard
  • the content still makes sense without color alone
  • inline controls do not accidentally block normal table navigation

Use this litmus test

A good custom cell should answer yes to both questions:

  1. Does this make the product table easier to understand or use?
  2. Would the table still behave predictably if this cell were duplicated across hundreds of rows?

If the second answer is no, the UI may be too heavy for a cell surface.

Where to go next

On this page