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
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
<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>
),
}}
/><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:
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
TextColumn::make('name', 'Name')->sortable()Frontend
<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>
),
}}
/><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:
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
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
<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();
}}
/><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.
<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>
)}
/><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
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
<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>
)}
</>
)}
/><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.
<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.")
/><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:
cellRenderers.{columnName}(React) /#cell-{columnName}slot (Vue) - highest priority, per-columnrenderCell(React) /#cellslot (Vue) - global override withdefaultRenderescape hatch- Built-in display pipeline - from PHP column configuration
For headers, the order is:
headerRenderers.{columnName}(React) /#header-{columnName}slot (Vue) - per-columnrenderHeader(React) /#headerslot (Vue) - global override- Built-in header - default sort indicators and click handling
For fully custom cell components that are reusable across tables, see Component Columns.