Skip to content

Overriding Cells

Inertia Table's PHP column definitions control how cells render by default - text, badges, dates, links, etc. But sometimes you need custom rendering for specific columns or the entire table. The override system lets you replace any cell or header with your own component while keeping the PHP-driven data pipeline intact.

Per-Column Cell Override

Use cellRenderers (React) or #cell-{columnName} slots (Vue) to replace the rendering of specific columns by name. The PHP column still provides the data - you just control how it's displayed.

PHP

php
class ServerTable extends Table
{
    protected function columns(): array
    {
        return [
            TextColumn::make('name', 'Name')->sortable(),
            EnumColumn::make('status', 'Status')->sortable(),
            TextColumn::make('ip', 'IP Address')->sortable(),
            Column::data('id'),
            Column::data('avatar_url'),
        ];
    }
}

Frontend

tsx
<InertiaTable
    tableData={servers}
    cellRenderers={{
        // Custom badge with your own component
        status: ({ row, value }) => (
            <MyCustomBadge status={value} color={row._status_enum_color} />
        ),
        // Avatar + name in one cell
        name: ({ row, value }) => (
            <div className="flex items-center gap-2">
                <img src={row.avatar_url} className="h-6 w-6 rounded-full" />
                <span className="font-medium">{String(value)}</span>
            </div>
        ),
    }}
/>
template
<InertiaTable :table-data="servers">
    <!-- Custom badge with your own component -->
    <template #cell-status="{ row, value }">
        <MyCustomBadge :status="value" :color="row._status_enum_color" />
    </template>
    <!-- Avatar + name in one cell -->
    <template #cell-name="{ row, value }">
        <div class="flex items-center gap-2">
            <img :src="row.avatar_url" class="h-6 w-6 rounded-full" />
            <span class="font-medium">{{ String(value) }}</span>
        </div>
    </template>
</InertiaTable>

In React, the key in cellRenderers matches the column name from PHP. In Vue, the column name is part of the slot name (e.g., #cell-status). Columns not overridden render normally using their PHP display configuration. Each renderer/slot receives:

tsx
interface CellRenderProps {
    row: Row;              // full row data including hidden columns
    value: unknown;        // the column's resolved value
    column: DynamicColumnDef;  // the column definition from PHP
    displays: CellDisplay[];   // the display pipeline from PHP
    rowIndex: number;      // zero-based row index
}

Per-Column Header Override

Use headerRenderers (React) or #header-{columnName} slots (Vue) to customise the header of specific columns. Useful for adding custom sort indicators, tooltips, or icons to column headers.

PHP

php
TextColumn::make('name', 'Name')->sortable()

Frontend

tsx
<InertiaTable
    tableData={servers}
    headerRenderers={{
        name: ({ column, sortState, onSort }) => (
            <div
                onClick={() => onSort(column.sort_key)}
                className="cursor-pointer flex items-center gap-1"
            >
                {column.header}
                {sortState.active && (sortState.direction === 'asc' ? ' ↑' : ' ↓')}
            </div>
        ),
    }}
/>
template
<InertiaTable :table-data="servers">
    <template #header-name="{ column, sortState, onSort }">
        <div
            @click="onSort(column.sort_key)"
            class="cursor-pointer flex items-center gap-1"
        >
            {{ column.header }}
            <template v-if="sortState.active">{{ sortState.direction === 'asc' ? ' ↑' : ' ↓' }}</template>
        </div>
    </template>
</InertiaTable>

Each header renderer receives:

tsx
interface HeaderRenderProps {
    column: DynamicColumnDef;
    sortState: SortState;           // { active: boolean, direction: 'asc' | 'desc' | null }
    onSort: (sortKey: string) => void;
    index: number;                  // zero-based column index
}

Global Cell Override

Use renderCell (React) or the #cell slot (Vue) to intercept the rendering of every cell. This is useful for applying consistent logic across all columns - like conditional formatting or wrapping cells in a tooltip.

The defaultRender function falls back to the normal display pipeline, so you only need to handle the cases you care about.

PHP

php
class ServerTable extends Table
{
    protected function columns(): array
    {
        return [
            TextColumn::make('name', 'Name')->sortable(),
            EnumColumn::make('status', 'Status')->sortable(),
            DateTimeColumn::make('updated_at', 'Last Seen')->sortable(),
        ];
    }
}

Frontend

tsx
<InertiaTable
    tableData={servers}
    renderCell={({ row, value, column, defaultRender }) => {
        // Custom rendering for one column
        if (column.name === 'status') {
            return <MyStatusBadge status={value} />;
        }

        // Wrap another column in a tooltip
        if (column.name === 'name') {
            return (
                <Tooltip content={`Server ID: ${row.id}`}>
                    {defaultRender()}
                </Tooltip>
            );
        }

        // Everything else renders normally
        return defaultRender();
    }}
/>
template
<InertiaTable :table-data="servers">
    <template #cell="{ row, value, column, defaultRender }">
        <!-- Custom rendering for one column -->
        <MyStatusBadge v-if="column.name === 'status'" :status="value" />
        <!-- Wrap another column in a tooltip -->
        <Tooltip v-else-if="column.name === 'name'" :content="`Server ID: ${row.id}`">
            <component :is="defaultRender" />
        </Tooltip>
        <!-- Everything else renders normally -->
        <component v-else :is="defaultRender" />
    </template>
</InertiaTable>

Global Header Override

Use renderHeader (React) or the #header slot (Vue) to customise every column header. For per-column overrides, use headerRenderers (React) or #header-{columnName} slots (Vue) instead.

tsx
<InertiaTable
    tableData={servers}
    renderHeader={({ column, sortState, onSort, index }) => (
        <div
            onClick={column.sortable ? () => onSort(column.sort_key) : undefined}
            className={column.sortable ? 'cursor-pointer' : ''}
        >
            <span className="uppercase text-xs tracking-wide">{column.header}</span>
            {sortState.active && (
                <span className="ml-1">{sortState.direction === 'asc' ? '↑' : '↓'}</span>
            )}
        </div>
    )}
/>
template
<InertiaTable :table-data="servers">
    <template #header="{ column, sortState, onSort }">
        <div
            @click="column.sortable ? onSort(column.sort_key) : undefined"
            :class="column.sortable ? 'cursor-pointer' : ''"
        >
            <span class="uppercase text-xs tracking-wide">{{ column.header }}</span>
            <span v-if="sortState.active" class="ml-1">{{ sortState.direction === 'asc' ? '↑' : '↓' }}</span>
        </div>
    </template>
</InertiaTable>

Row Wrapper

Use renderRow (React) or the #row slot (Vue) to wrap or augment individual table rows. In React, the children prop contains the default <tr> with all its cells. In Vue, use <slot /> to render the default row. You can wrap it, add sibling rows, or conditionally modify it.

PHP

php
class ServerTable extends Table
{
    protected function columns(): array
    {
        return [
            TextColumn::make('name', 'Name')->sortable(),
            TextColumn::make('ip', 'IP Address'),
            Column::data('id'),
            Column::data('details'),   // hidden data for expanded view
        ];
    }
}

Frontend

tsx
<InertiaTable
    tableData={servers}
    renderRow={({ row, children, rowIndex }) => (
        <>
            {children}
            {row.details && (
                <tr className="bg-gray-50 dark:bg-gray-800">
                    <td colSpan={100} className="px-4 py-3 text-sm text-gray-600 dark:text-gray-400">
                        {String(row.details)}
                    </td>
                </tr>
            )}
        </>
    )}
/>
template
<InertiaTable :table-data="servers">
    <template #row="{ row, rowIndex }">
        <slot />
        <tr v-if="row.details" class="bg-gray-50 dark:bg-gray-800">
            <td colspan="100" class="px-4 py-3 text-sm text-gray-600 dark:text-gray-400">
                {{ String(row.details) }}
            </td>
        </tr>
    </template>
</InertiaTable>

Null & Empty Text

Control the default text shown for null cell values and empty tables without writing a custom renderer.

tsx
<InertiaTable
    tableData={servers}
    nullText="N/A"          // shown when a cell value is null (default: "-")
    emptyText="No servers"  // shown when there are no rows (default: "No results found.")
/>
template
<InertiaTable
    :table-data="servers"
    null-text="N/A"
    empty-text="No servers"
/>

For fully custom empty states, use renderEmpty (React) or the #empty slot (Vue) - see Custom Pagination.

Override Priority

When multiple overrides are set, the table resolves cells in this order:

  1. cellRenderers.{columnName} (React) / #cell-{columnName} slot (Vue) - highest priority, per-column
  2. renderCell (React) / #cell slot (Vue) - global override with defaultRender escape hatch
  3. Built-in display pipeline - from PHP column configuration

For headers, the order is:

  1. headerRenderers.{columnName} (React) / #header-{columnName} slot (Vue) - per-column
  2. renderHeader (React) / #header slot (Vue) - global override
  3. Built-in header - default sort indicators and click handling

For fully custom cell components that are reusable across tables, see Component Columns.