Skip to content

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.