Custom Cell Renderers in React Tables

Cell RenderersCustomizationTutorial

Plain text in a data grid only goes so far. Learn how to render status badges, progress bars, avatars, and action buttons directly inside React table cells with Simple Table's cellRenderer API—plus when to reach for valueFormatter instead.

A column of raw strings like active, 0.82, or https://…/avatar.png tells users almost nothing at a glance. A green "Active" pill, an 82% progress bar, and a round avatar tell the whole story instantly. That transformation is what a cell renderer does: it takes the raw value and returns a React component to display instead.

Cell renderers are where a data grid stops looking like a spreadsheet and starts looking like your product. In Simple Table, every column can define its own cellRenderer, giving you complete control over what each cell renders—badges, buttons, charts, links, anything React can produce.

This guide covers the cellRenderer API and its parameters, when to use valueFormatter instead, copy-paste recipes for the most common patterns, and the performance rules that keep large grids fast.

cellRenderer vs valueFormatter: Which One?

Simple Table gives you two ways to change how a cell looks. Picking the right one matters for performance.

valueFormatter

Returns a formatted string. Best for text transforms.

Use for

  • • Currency, dates, percentages
  • • Number/string formatting
  • • Anything that stays plain text

More performant—prefer it whenever the output is just text.

cellRenderer

Returns a React node. Best for visual / interactive cells.

Use for

  • • Badges, pills, progress bars
  • • Avatars, icons, images, links
  • • Buttons and interactive controls

Runs frequently—keep it lightweight (see performance section).

Rule of thumb: if the output is text, use valueFormatter. If it needs markup, color, or interactivity, use cellRenderer. You can even combine them—more on that below.

Basic Usage: A Status Badge

Add a cellRenderer function to any column in defaultHeaders. It receives the cell's data and returns a React node. In React, columns are typed ReactHeaderObject.

React TSX
1import { SimpleTable } from "@simple-table/react";
2import type { ReactHeaderObject, CellRendererProps } from "@simple-table/react";
3import "@simple-table/react/styles.css";
4
5const headers: ReactHeaderObject[] = [
6 { accessor: "name", label: "Name", width: 200, type: "string" },
7 {
8 accessor: "status",
9 label: "Status",
10 width: 140,
11 type: "string",
12 cellRenderer: ({ value }: CellRendererProps) => {
13 const isActive = value === "active";
14 return (
15 <span
16 style={{
17 padding: "2px 10px",
18 borderRadius: "9999px",
19 fontSize: "12px",
20 fontWeight: 600,
21 backgroundColor: isActive ? "#DCFCE7" : "#FEE2E2",
22 color: isActive ? "#166534" : "#991B1B",
23 }}
24 >
25 {isActive ? "Active" : "Inactive"}
26 </span>
27 );
28 },
29 },
30];
31
32export default function UsersTable({ rows }) {
33 return <SimpleTable defaultHeaders={headers} rows={rows} height="500px" />;
34}

The CellRendererProps parameters

Your renderer receives a single object with everything you need:

value — the raw cell value (same as row[accessor]).

row — the full row object, so a renderer can depend on multiple columns.

formattedValue — the output of valueFormatter if the column defines one, so you can wrap formatted text in custom markup.

accessor, colIndex, rowIndex, theme, and rowPath (the path through nested data, e.g. [0, "teams", 1]).

See It In Action

The table below uses cell renderers for team-member avatars and progress bars—proof that "cells" can be full React components.

Copy-Paste Recipes

Progress bar

Turn a 0–1 (or 0–100) number into a visual bar:

React TSX
1{
2 accessor: "completion",
3 label: "Progress",
4 width: 160,
5 type: "number",
6 cellRenderer: ({ value }: CellRendererProps) => {
7 const pct = Math.round(Number(value) * 100);
8 return (
9 <div style={{ display: "flex", alignItems: "center", gap: 8 }}>
10 <div style={{ flex: 1, height: 8, borderRadius: 9999, background: "#E5E7EB" }}>
11 <div
12 style={{
13 width: `${pct}%`,
14 height: 8,
15 borderRadius: 9999,
16 background: pct >= 80 ? "#16A34A" : pct >= 40 ? "#F59E0B" : "#EF4444",
17 }}
18 />
19 </div>
20 <span style={{ fontSize: 12, color: "#6B7280" }}>{pct}%</span>
21 </div>
22 );
23 },
24}

Avatar with name

Use row to combine multiple fields into one cell:

React TSX
1{
2 accessor: "name",
3 label: "User",
4 width: 220,
5 type: "string",
6 cellRenderer: ({ row }: CellRendererProps) => (
7 <div style={{ display: "flex", alignItems: "center", gap: 8 }}>
8 <img
9 src={row.avatarUrl as string}
10 alt=""
11 style={{ width: 28, height: 28, borderRadius: "50%" }}
12 />
13 <div style={{ display: "flex", flexDirection: "column" }}>
14 <span style={{ fontWeight: 600 }}>{row.name as string}</span>
15 <span style={{ fontSize: 12, color: "#6B7280" }}>{row.email as string}</span>
16 </div>
17 </div>
18 ),
19}

Action buttons

Render interactive controls right in the grid:

React TSX
1{
2 accessor: "id",
3 label: "Actions",
4 width: 160,
5 type: "string",
6 cellRenderer: ({ row }: CellRendererProps) => (
7 <div style={{ display: "flex", gap: 8 }}>
8 <button onClick={() => onEdit(row.id)} className="st-btn st-btn-secondary">
9 Edit
10 </button>
11 <button onClick={() => onDelete(row.id)} className="st-btn st-btn-danger">
12 Delete
13 </button>
14 </div>
15 ),
16}

Conditional formatting from multiple columns

Because row is the whole record, a cell can react to other fields—for example, flag a balance as overdue:

React TSX
1{
2 accessor: "balance",
3 label: "Balance",
4 width: 140,
5 type: "number",
6 align: "right",
7 cellRenderer: ({ value, row }: CellRendererProps) => {
8 const overdue = row.status === "overdue";
9 return (
10 <span style={{ color: overdue ? "#DC2626" : "#111827", fontWeight: overdue ? 700 : 400 }}>
11 {new Intl.NumberFormat("en-US", { style: "currency", currency: "USD" }).format(
12 Number(value)
13 )}
14 </span>
15 );
16 },
17}

Wrapping Formatted Text with formattedValue

You don't have to choose between the two. Define a valueFormatter for the text transform, then read formattedValue in your cellRenderer to wrap that formatted string in markup—no need to re-implement the formatting logic:

React TSX
1{
2 accessor: "revenue",
3 label: "Revenue",
4 width: 140,
5 type: "number",
6 align: "right",
7 valueFormatter: ({ value }) =>
8 new Intl.NumberFormat("en-US", { style: "currency", currency: "USD" }).format(
9 Number(value)
10 ),
11 cellRenderer: ({ value, formattedValue }: CellRendererProps) => (
12 <span style={{ color: Number(value) > 0 ? "#16A34A" : "#DC2626" }}>
13 {formattedValue}
14 </span>
15 ),
16}

Bonus: the same idea applies to headers. Use headerRenderer on a column to render custom header content (icons, tooltips, multi-line labels) the same way cellRenderer handles cells.

Performance Best Practices

Cell renderers run often—on scroll, sort, and re-render—so a slow renderer multiplied across visible rows adds up. Keep them cheap:

  • Prefer valueFormatter for plain text—it's lighter than a React node.
  • Avoid expensive work inside the renderer (no heavy computation, network calls, or large object creation per cell).
  • Memoize complex components and hoist static styles/objects out of the render path.
  • Pair with virtualization—only visible rows render, so a tight renderer scales to a million rows.

Make Your Cells Tell the Story

Cell renderers turn a generic grid into an interface that matches your product. Reach for valueFormatter when you only need formatted text, and cellRenderer when a cell needs color, layout, or interactivity—then keep both lean so the grid stays fast.

Want to see how far custom rendering can go? Check out how we replicated a full CRM UI with Simple Table, or learn the broader theming system.

Ready to build rich, interactive table cells?

Simple Table's cellRenderer gives you full React control over every cell—badges, charts, buttons, and more—for free. Start customizing your data grid in minutes.