shadcn/ui Integration
Use useTable with shadcn components for a fully styled table that stays in sync with your PHP definitions.
ShadTable Component
tsx
import { useTable } from 'inertia-table-react';
import { Table, TableHeader, TableBody, TableHead, TableRow, TableCell } from '@/components/ui/table';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
export default function ShadTable({ tableData, onRowClick, ...rest }) {
const { columns, searchTerm, onSearch, onPageChange, isProcessing } = useTable({ tableData, ...rest });
const hasData = tableData.data.length > 0;
return (
<div className="rounded-lg border bg-card text-card-foreground shadow-sm">
{tableData.searchable && (
<div className="p-4">
<Input
value={searchTerm}
onChange={(e) => onSearch(e.target.value)}
placeholder="Search..."
className="max-w-sm"
/>
</div>
)}
<div className={`transition-opacity duration-150${isProcessing ? ' opacity-50 pointer-events-none' : ''}`}>
<Table>
<TableHeader>
<TableRow>
{columns.map((col) => (
<TableHead key={col.id}>{col.renderHeader()}</TableHead>
))}
</TableRow>
</TableHeader>
<TableBody>
{hasData ? (
tableData.data.map((row, i) => (
<TableRow
key={row.id}
onClick={onRowClick ? () => onRowClick(row) : undefined}
className={onRowClick ? 'cursor-pointer' : ''}
>
{columns.map((col) => (
<TableCell key={col.id}>{col.renderCell(row, i)}</TableCell>
))}
</TableRow>
))
) : (
<TableRow>
<TableCell colSpan={columns.length} className="h-24 text-center text-muted-foreground">
No results found.
</TableCell>
</TableRow>
)}
</TableBody>
</Table>
</div>
{hasData && (
<div className="flex items-center justify-between px-4 py-4 border-t">
<p className="text-sm text-muted-foreground">
Showing <strong>{tableData.meta.from}</strong> to <strong>{tableData.meta.to}</strong>
{tableData.meta.total != null && <> of <strong>{tableData.meta.total}</strong> results</>}
</p>
<div className="flex gap-2">
<Button variant="outline" size="sm" disabled={!tableData.links.prev || isProcessing}
onClick={() => onPageChange(tableData.meta.current_page - 1)}>Previous</Button>
<Button variant="outline" size="sm" disabled={!tableData.links.next || isProcessing}
onClick={() => onPageChange(tableData.meta.current_page + 1)}>Next</Button>
</div>
</div>
)}
</div>
);
}vue
<script setup lang="ts">
import { useTable } from 'inertia-table-vue';
import { Table, TableHeader, TableBody, TableHead, TableRow, TableCell } from '@/components/ui/table';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { useSlots } from 'vue';
const props = defineProps<{
tableData: any;
onRowClick?: (row: any) => void;
}>();
const slots = useSlots();
const { columns, searchTerm, onSearch, onPageChange, isProcessing } = useTable(props, slots);
const hasData = computed(() => props.tableData.data.length > 0);
</script>
<template>
<div class="rounded-lg border bg-card text-card-foreground shadow-sm">
<div v-if="tableData.searchable" class="p-4">
<Input
:model-value="searchTerm"
@update:model-value="onSearch"
placeholder="Search..."
class="max-w-sm"
/>
</div>
<div :class="['transition-opacity duration-150', isProcessing ? 'opacity-50 pointer-events-none' : '']">
<Table>
<TableHeader>
<TableRow>
<TableHead v-for="col in columns" :key="col.id">
{{ col.renderHeader() }}
</TableHead>
</TableRow>
</TableHeader>
<TableBody>
<template v-if="hasData">
<TableRow
v-for="(row, i) in tableData.data"
:key="row.id"
:class="onRowClick ? 'cursor-pointer' : ''"
@click="onRowClick?.(row)"
>
<TableCell v-for="col in columns" :key="col.id">
<component :is="col.renderCell(row, i)" />
</TableCell>
</TableRow>
</template>
<template v-else>
<TableRow>
<TableCell :colspan="columns.length" class="h-24 text-center text-muted-foreground">
No results found.
</TableCell>
</TableRow>
</template>
</TableBody>
</Table>
</div>
<div v-if="hasData" class="flex items-center justify-between px-4 py-4 border-t">
<p class="text-sm text-muted-foreground">
Showing <strong>{{ tableData.meta.from }}</strong> to <strong>{{ tableData.meta.to }}</strong>
<template v-if="tableData.meta.total != null">
of <strong>{{ tableData.meta.total }}</strong> results
</template>
</p>
<div class="flex gap-2">
<Button variant="outline" size="sm" :disabled="!tableData.links.prev || isProcessing"
@click="onPageChange(tableData.meta.current_page - 1)">Previous</Button>
<Button variant="outline" size="sm" :disabled="!tableData.links.next || isProcessing"
@click="onPageChange(tableData.meta.current_page + 1)">Next</Button>
</div>
</div>
</div>
</template>Usage
tsx
import ShadTable from '@/components/ShadTable';
export default function Servers({ servers }) {
return <ShadTable tableData={servers} />;
}vue
<script setup lang="ts">
import ShadTable from '@/components/ShadTable.vue';
defineProps<{ servers: any }>();
</script>
<template>
<ShadTable :table-data="servers" />
</template>The useTable hook handles all logic - search, sort, pagination, column building, display type rendering. The shadcn component only controls markup and styling. When the PHP table definition changes, the frontend adapts automatically.