Component Columns
When the built-in column types and display modifiers don't fit your needs, you can delegate cell rendering to your own frontend component. The column definition stays in PHP - only the rendering is custom.
PHP Setup
Use ComponentColumn::create() or the ->component() modifier on any column:
use Forjed\InertiaTable\Columns\ComponentColumn;
// Typed column - third argument is the component name
ComponentColumn::create('status', 'Status', 'StatusIndicator')
// Or as a display modifier on a base column
Column::make('status', 'Status')->component('StatusIndicator')The first argument is the column name (data key), the second is the header, and the third is the name you register the component under on the frontend.
Registering Components
Register your component at app boot:
// app.tsx
import { registerCellComponent } from 'inertia-table-react';
registerCellComponent('StatusIndicator', ({ value }) => (
<div className="flex items-center gap-2">
<div className={`h-2 w-2 rounded-full ${value === 'active' ? 'bg-green-500' : 'bg-gray-300'}`} />
<span>{String(value)}</span>
</div>
));// app.ts
import { registerCellComponent } from 'inertia-table-vue';
import { h } from 'vue';
registerCellComponent('StatusIndicator', ({ value }) =>
h('div', { class: 'flex items-center gap-2' }, [
h('div', { class: `h-2 w-2 rounded-full ${value === 'active' ? 'bg-green-500' : 'bg-gray-300'}` }),
h('span', String(value)),
])
);Component Props
Every custom component receives three props:
interface CellComponentProps {
row: Row; // full row data including hidden columns
value: unknown; // the column's resolved value
column: string; // the column name this component is bound to
}Use value for the bound column data and row when you need to access other fields. The column prop tells you which field the component is rendering, making components reusable across different columns.
Reusable Components
Because components receive value instead of reading a hardcoded field, the same component works on any column:
// PHP - same component, different columns
ComponentColumn::create('status', 'Server Status', 'StatusIndicator')
ComponentColumn::create('health', 'Health Check', 'StatusIndicator')
ComponentColumn::create('connectivity', 'Network', 'StatusIndicator')The StatusIndicator component doesn't need to know which field it's bound to - it just uses value.
Accessing Related Data
Use row to access hidden data columns or other fields alongside the bound value:
// PHP
protected function columns(): array
{
return [
ComponentColumn::create('progress', 'Progress', 'ProgressBar'),
Column::data('target'), // hidden - accessible via row.target
Column::data('id'),
];
}registerCellComponent('ProgressBar', ({ value, row }) => {
const current = Number(value);
const target = Number(row.target);
const percent = target > 0 ? Math.round((current / target) * 100) : 0;
return (
<div className="flex items-center gap-2">
<div className="h-2 w-full rounded-full bg-gray-200 dark:bg-gray-700">
<div
className="h-2 rounded-full bg-blue-500"
style={{ width: `${percent}%` }}
/>
</div>
<span className="text-xs text-gray-500 whitespace-nowrap">{percent}%</span>
</div>
);
});// app.ts
import { registerCellComponent } from 'inertia-table-vue';
import { h } from 'vue';
registerCellComponent('ProgressBar', ({ value, row }) => {
const current = Number(value);
const target = Number(row.target);
const percent = target > 0 ? Math.round((current / target) * 100) : 0;
return h('div', { class: 'flex items-center gap-2' }, [
h('div', { class: 'h-2 w-full rounded-full bg-gray-200 dark:bg-gray-700' }, [
h('div', {
class: 'h-2 rounded-full bg-blue-500',
style: { width: `${percent}%` },
}),
]),
h('span', { class: 'text-xs text-gray-500 whitespace-nowrap' }, `${percent}%`),
]);
});TypeScript Typing
Use CellComponentProps for type safety:
import type { CellComponentProps } from 'inertia-table-react';
interface ServerRow {
id: number;
status: string;
uptime: number;
}
function StatusIndicator({ value, row }: CellComponentProps<ServerRow>) {
// value is typed as unknown - cast as needed
const status = String(value);
// row is typed as ServerRow
const uptime = row.uptime;
return (
<div className="flex items-center gap-2">
<div className={`h-2 w-2 rounded-full ${status === 'active' ? 'bg-green-500' : 'bg-red-500'}`} />
<span>{status}</span>
<span className="text-xs text-gray-400">({uptime}h)</span>
</div>
);
}
registerCellComponent('StatusIndicator', StatusIndicator);// app.ts
import { registerCellComponent } from 'inertia-table-vue';
import { defineComponent, h } from 'vue';
import type { CellComponentProps } from 'inertia-table-vue';
interface ServerRow {
id: number;
status: string;
uptime: number;
}
const StatusIndicator = defineComponent({
props: {
value: { type: null, required: true },
row: { type: Object as () => ServerRow, required: true },
column: { type: String, required: true },
},
setup(props) {
return () => {
const status = String(props.value);
const uptime = props.row.uptime;
return h('div', { class: 'flex items-center gap-2' }, [
h('div', { class: `h-2 w-2 rounded-full ${status === 'active' ? 'bg-green-500' : 'bg-red-500'}` }),
h('span', status),
h('span', { class: 'text-xs text-gray-400' }, `(${uptime}h)`),
]);
};
},
});
registerCellComponent('StatusIndicator', StatusIndicator);Inline Alternative
If you don't need a reusable component, you can define custom rendering inline without registration - use the cellRenderers prop in React or #cell-{columnName} scoped slots in Vue. Use a standard column in PHP:
Column::make('status', 'Status')->sortable()Then render it on the frontend:
<InertiaTable
tableData={servers}
cellRenderers={{
status: ({ value, row }) => (
<div className="flex items-center gap-2">
<div className={`h-2 w-2 rounded-full ${value === 'active' ? 'bg-green-500' : 'bg-gray-300'}`} />
<span>{String(value)}</span>
</div>
),
}}
/><template>
<InertiaTable :table-data="servers">
<template #cell-status="{ value, row }">
<div class="flex items-center gap-2">
<div :class="`h-2 w-2 rounded-full ${value === 'active' ? 'bg-green-500' : 'bg-gray-300'}`" />
<span>{{ String(value) }}</span>
</div>
</template>
</InertiaTable>
</template>In React, the key in cellRenderers matches the column name from PHP. In Vue, use the #cell-{columnName} scoped slot. Both receive { row, value, column, displays, rowIndex } - see Overriding Cells for the full type.
When to use which:
ComponentColumn+registerCellComponent- reusable across tables, defined once at bootcellRenderers(React) /#cell-{columnName}slots (Vue) - one-off, page-specific, no registration needed
Combining with Other Modifiers
ComponentColumn supports the same fluent methods as other columns:
ComponentColumn::create('status', 'Status', 'StatusIndicator')
->sortable()
->fit()Note that ->component() as a display modifier replaces the entire cell rendering - other display modifiers like ->text() or ->badge() on the same column are ignored when a component display is present.