Data Table
The DataTable component is a flexible, sortable, and paginated table built with @tanstack/react-table.
It supports loading states, empty messages, custom styling, and row selection.
import { DataTable } from "tawuniya/components";
👉 View in Storybook
Props​
| Prop | Type | Default | Description |
|---|---|---|---|
columns | ColumnDef<TData, TValue>[] | - | Table column definitions. |
data | TData[] | - | Array of row data. |
loading | boolean | false | If true, shows TableSkeleton. |
classNameWrapper | string | - | Classes for the wrapper container. |
classNameHeadRow | string | - | Classes for the header row. |
classNameHeadCell | string | - | Classes for each header cell. |
classNameBodyRow | string | - | Classes for each body row. |
classNameBodyCell | string | - | Classes for each body cell. |
classNamePagination | string | - | Classes for pagination. |
hidePagination | boolean | false | Hides pagination controls if true. |
skeleton | { rowSpan?: number; colSpan?: number } | - | Customizes skeleton rows when loading. |
empty | { icon?: ReactNode; title?: string; description?: string } | - | Custom empty state message. |
paginationTotal | number | 0 | Total number of items for pagination. |
onSelectedRowsChange | Dispatch<SetStateAction<TData[]>> | [] | Callback triggered when selected rows change. |
resetSelection | object | null | Resets row selection when empty object or array. |
onRowClick | (tableRow: Row<TData>, rowItem: TData) => void | undefined | Triggered when a row is clicked. |
Columns​
interface Expense {
id: string;
label: string;
note: string;
category: string;
type: string;
}
export const columns: ColumnDef<Expense>[] = [
{
id: "select",
size: 40,
header: ({ table }) => (
<Checkbox
checked={
table.getIsAllPageRowsSelected() ||
(table.getIsSomePageRowsSelected() && "indeterminate")
}
onCheckedChange={(value) => table.toggleAllPageRowsSelected(!!value)}
aria-label="Select all"
className="translate-y-0.5"
/>
),
cell: ({ row }) => (
<Checkbox
checked={row.getIsSelected()}
onCheckedChange={(value) => row.toggleSelected(!!value)}
aria-label="Select row"
className="translate-y-0.5"
/>
),
enableSorting: false,
enableHiding: false,
},
{
accessorKey: "label",
header: ({ column }) => (
<DataTableColumnHeader column={column} title="Label" />
),
cell: ({ row }) => (
<div className="w-[150px] capitalize">{row.getValue("label")}</div>
),
},
{
accessorKey: "note",
header: ({ column }) => (
<DataTableColumnHeader column={column} title="Note" />
),
cell: ({ row }) => {
return (
<div className="flex space-x-2">
<span className="max-w-125 truncate font-medium capitalize">
{row.getValue("note")}
</span>
</div>
);
},
enableSorting: false, // Disable sorting for this column
},
{
accessorKey: "category",
header: ({ column }) => (
<DataTableColumnHeader column={column} title="Category" />
),
cell: ({ row }) => {
return (
<div className="flex w-25 items-center">
<span className="capitalize"> {row.getValue("category")}</span>
</div>
);
},
filterFn: (row, id, value) => {
return value.includes(row.getValue(id));
},
},
{
accessorKey: "type",
header: ({ column }) => (
<DataTableColumnHeader column={column} title="Type" />
),
cell: ({ row }) => {
const type = row.getValue("type");
return (
<div className="flex w-25 items-center">
{type === "income" ? (
<TrendingUp size={20} className="mr-2 text-green-500" />
) : (
<TrendingDown size={20} className="mr-2 text-red-500" />
)}
<span className="capitalize"> {row.getValue("type")}</span>
</div>
);
},
filterFn: (row, id, value) => {
return value.includes(row.getValue(id));
},
},
{
id: "actions",
header: ({ column }) => (
<DataTableColumnHeader column={column} title="Action" />
),
cell: () => (
<>
<Button size="sm" variant="smooth_destructive">
<Trash size={16} />
Delete
</Button>
</>
),
},
];
Usage​
Basic Example​
<DataTable
columns={columns}
data={[
{
id: "1",
label: "salary",
note: "monthly salary",
category: "income",
type: "income",
},
]}
/>
With Loading Skeleton​
<DataTable
columns={columns}
data={[
{
id: "1",
label: "salary",
note: "monthly salary",
category: "income",
type: "income",
},
]}
loading
skeleton={{ rowSpan: 5, colSpan: 1 }}
/>
With Empty State​
<DataTable
columns={columns}
data={[]}
empty={{
icon: <EmptyIcon />,
title: "No Records",
description: "There are no entries to display.",
}}
/>
With Pagination​
<DataTable
columns={columns}
data={[
{
id: "1",
label: "salary",
note: "monthly salary",
category: "income",
type: "income",
},
]}
paginationTotal={100}
/>
With Pagination Hidden​
<DataTable
columns={columns}
data={[
{
id: "1",
label: "salary",
note: "monthly salary",
category: "income",
type: "income",
},
]}
hidePagination={true}
/>
With Row Selection & Reset Selection​
const [selectedRows, setSelectedRows] = useState<any>([]);
const [resetSelectedRows, setResetSelectedRows] = useState<object | null>(null);
const onDelete = () => {
// Delete Rows Logic
setResetSelectedRows({});
};
const data = useMemo(() => {
return [
{
id: "1",
label: "salary",
note: "monthly salary",
category: "income",
type: "income",
},
]
}, [])
<div>
{selectedRows.length > 0 && <Button onClick={onDelete}>Delete</Button>}
<DataTable
columns={columns}
data={data}
onSelectedRowsChange={setSelectedRows}
resetSelection={resetSelectedRows}
/>
</div>;
With Row Click​
<DataTable
columns={columns}
data={[
{
id: "1",
label: "salary",
note: "monthly salary",
category: "income",
type: "income",
},
]}
onRowClick={(tableRow, rowItem) => console.log(tableRow, rowItem)}
/>
Features​
- Sorting: Columns can be sorted with click or keyboard (Enter/Space).
- Pagination: Supports manual pagination with a separate Pagination component.
- Loading State: Displays skeleton rows when loading.
- Empty State: Shows a customizable empty message when no data is available.
- Custom Styling: Pass className props to table, rows, and cells.
Notes​
- Empty messages and skeletons improve user experience during data fetching.