Display
Data Table
A sortable, filterable, paginated table built with TanStack Table and the Table primitives.
Examples
RTL
Right-to-left layout support for languages such as Arabic and Hebrew.
Translations are AI-generated for demonstration and may be imperfect.
| الحالة | المبلغ | |||
|---|---|---|---|---|
ناجح | ken99@example.com | ٣١٦٫٠٠ US$ | ||
ناجح | Abe45@example.com | ٢٤٢٫٠٠ US$ | ||
قيد المعالجة | Monserrat44@example.com | ٨٣٧٫٠٠ US$ | ||
ناجح | Silas22@example.com | ٨٧٤٫٠٠ US$ | ||
فشل | carmella@example.com | ٧٢١٫٠٠ US$ |
0 من 5 صف(وف) محدد.
387 lines
import { Button } from "@codefast/ui/button";
import { Checkbox } from "@codefast/ui/checkbox";
import {
DropdownMenu,
DropdownMenuCheckboxItem,
DropdownMenuContent,
DropdownMenuGroup,
DropdownMenuItem,
DropdownMenuLabel,
DropdownMenuSeparator,
DropdownMenuTrigger,
} from "@codefast/ui/dropdown-menu";
import { Input } from "@codefast/ui/input";
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@codefast/ui/table";
import {
flexRender,
getCoreRowModel,
getFilteredRowModel,
getPaginationRowModel,
getSortedRowModel,
useReactTable,
} from "@tanstack/react-table";
import type { ColumnDef, ColumnFiltersState, SortingState, VisibilityState } from "@tanstack/react-table";
import { ArrowUpDown, ChevronDown, MoreHorizontal } from "lucide-react";
import * as React from "react";
import type { Translations } from "#/features/components-catalog/components/detail/language";
import { useTranslation } from "#/features/components-catalog/components/detail/language-context";
type DataTableValues = {
filterEmails: string;
columns: string;
status: string;
email: string;
amount: string;
actions: string;
copyPaymentId: string;
viewCustomer: string;
viewPaymentDetails: string;
selectAll: string;
selectRow: string;
openMenu: string;
noResults: string;
rowsSelected: string;
rowsSelectedSuffix: string;
previous: string;
next: string;
success: string;
processing: string;
failed: string;
pending: string;
};
const translations: Translations<DataTableValues> = {
en: {
dir: "ltr",
values: {
filterEmails: "Filter emails...",
columns: "Columns",
status: "Status",
email: "Email",
amount: "Amount",
actions: "Actions",
copyPaymentId: "Copy payment ID",
viewCustomer: "View customer",
viewPaymentDetails: "View payment details",
selectAll: "Select all",
selectRow: "Select row",
openMenu: "Open menu",
noResults: "No results.",
rowsSelected: "of",
rowsSelectedSuffix: "row(s) selected.",
previous: "Previous",
next: "Next",
success: "Success",
processing: "Processing",
failed: "Failed",
pending: "Pending",
},
},
ar: {
dir: "rtl",
values: {
filterEmails: "تصفية البريد الإلكتروني...",
columns: "الأعمدة",
status: "الحالة",
email: "البريد الإلكتروني",
amount: "المبلغ",
actions: "الإجراءات",
copyPaymentId: "نسخ معرف الدفع",
viewCustomer: "عرض العميل",
viewPaymentDetails: "عرض تفاصيل الدفع",
selectAll: "تحديد الكل",
selectRow: "تحديد الصف",
openMenu: "فتح القائمة",
noResults: "لا توجد نتائج.",
rowsSelected: "من",
rowsSelectedSuffix: "صف(وف) محدد.",
previous: "السابق",
next: "التالي",
success: "ناجح",
processing: "قيد المعالجة",
failed: "فشل",
pending: "قيد الانتظار",
},
},
he: {
dir: "rtl",
values: {
filterEmails: "סנן אימיילים...",
columns: "עמודות",
status: "סטטוס",
email: "אימייל",
amount: "סכום",
actions: "פעולות",
copyPaymentId: "העתק מזהה תשלום",
viewCustomer: "צפה בלקוח",
viewPaymentDetails: "צפה בפרטי תשלום",
selectAll: "בחר הכל",
selectRow: "בחר שורה",
openMenu: "פתח תפריט",
noResults: "אין תוצאות.",
rowsSelected: "מתוך",
rowsSelectedSuffix: "שורות נבחרו.",
previous: "הקודם",
next: "הבא",
success: "הצליח",
processing: "מעבד",
failed: "נכשל",
pending: "ממתין",
},
},
};
type Payment = {
id: string;
amount: number;
status: "pending" | "processing" | "success" | "failed";
email: string;
};
const data: Array<Payment> = [
{
id: "m5gr84i9",
amount: 316,
status: "success",
email: "ken99@example.com",
},
{
id: "3u1reuv4",
amount: 242,
status: "success",
email: "Abe45@example.com",
},
{
id: "derv1ws0",
amount: 837,
status: "processing",
email: "Monserrat44@example.com",
},
{
id: "5kma53ae",
amount: 874,
status: "success",
email: "Silas22@example.com",
},
{
id: "bhqecj4p",
amount: 721,
status: "failed",
email: "carmella@example.com",
},
];
export function DataTableRtl() {
const { t, dir, language } = useTranslation(translations, "ar");
const [sorting, setSorting] = React.useState<SortingState>([]);
const [columnFilters, setColumnFilters] = React.useState<ColumnFiltersState>([]);
const [columnVisibility, setColumnVisibility] = React.useState<VisibilityState>({});
const [rowSelection, setRowSelection] = React.useState({});
const columns: Array<ColumnDef<Payment>> = React.useMemo(
() => [
{
id: "select",
header: ({ table }) => (
<Checkbox
checked={table.getIsAllPageRowsSelected() || (table.getIsSomePageRowsSelected() ? true : false)}
onCheckedChange={(value) => table.toggleAllPageRowsSelected(!!value)}
aria-label={t.selectAll}
/>
),
cell: ({ row }) => (
<Checkbox
checked={row.getIsSelected()}
onCheckedChange={(value) => row.toggleSelected(!!value)}
aria-label={t.selectRow}
/>
),
enableSorting: false,
enableHiding: false,
},
{
accessorKey: "status",
header: t.status,
cell: ({ row }) => {
const status = row.getValue("status") as string;
const statusMap: Record<string, string> = {
success: t.success,
processing: t.processing,
failed: t.failed,
pending: t.pending,
};
return <div className="capitalize">{statusMap[status]}</div>;
},
},
{
accessorKey: "email",
header: ({ column }) => {
return (
<Button variant="ghost" onClick={() => column.toggleSorting(column.getIsSorted() === "asc")}>
{t.email}
<ArrowUpDown />
</Button>
);
},
cell: ({ row }) => <div className="lowercase">{row.getValue("email")}</div>,
},
{
accessorKey: "amount",
header: () => <div className="text-start">{t.amount}</div>,
cell: ({ row }) => {
const amount = Number.parseFloat(row.getValue("amount"));
const formatted = new Intl.NumberFormat(dir === "rtl" ? "ar-SA" : "en-US", {
style: "currency",
currency: "USD",
}).format(amount);
return <div className="text-start font-medium">{formatted}</div>;
},
},
{
id: "actions",
enableHiding: false,
cell: ({ row }) => {
const payment = row.original;
return (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="ghost" size="icon-xs">
<span className="sr-only">{t.openMenu}</span>
<MoreHorizontal />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end" className="w-40" data-lang={dir === "rtl" ? language : undefined}>
<DropdownMenuGroup>
<DropdownMenuLabel>{t.actions}</DropdownMenuLabel>
<DropdownMenuItem onClick={() => void navigator.clipboard.writeText(payment.id)}>
{t.copyPaymentId}
</DropdownMenuItem>
</DropdownMenuGroup>
<DropdownMenuSeparator />
<DropdownMenuGroup>
<DropdownMenuItem>{t.viewCustomer}</DropdownMenuItem>
<DropdownMenuItem>{t.viewPaymentDetails}</DropdownMenuItem>
</DropdownMenuGroup>
</DropdownMenuContent>
</DropdownMenu>
);
},
},
],
[t, dir, language],
);
const table = useReactTable({
data,
columns,
onSortingChange: setSorting,
onColumnFiltersChange: setColumnFilters,
getCoreRowModel: getCoreRowModel(),
getPaginationRowModel: getPaginationRowModel(),
getSortedRowModel: getSortedRowModel(),
getFilteredRowModel: getFilteredRowModel(),
onColumnVisibilityChange: setColumnVisibility,
onRowSelectionChange: setRowSelection,
state: {
sorting,
columnFilters,
columnVisibility,
rowSelection,
},
});
return (
<div className="w-full">
<div className="flex items-center gap-2 py-4">
<Input
placeholder={t.filterEmails}
value={(table.getColumn("email")?.getFilterValue() as string) ?? ""}
onChange={(event) => table.getColumn("email")?.setFilterValue(event.target.value)}
className="max-w-sm"
/>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="outline" className="ms-auto">
{t.columns} <ChevronDown />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align={dir === "rtl" ? "start" : "end"} data-lang={dir === "rtl" ? language : undefined}>
<DropdownMenuGroup>
{table
.getAllColumns()
.filter((column) => column.getCanHide())
.map((column) => {
return (
<DropdownMenuCheckboxItem
key={column.id}
className="capitalize"
checked={column.getIsVisible()}
onCheckedChange={(value) => column.toggleVisibility(!!value)}
>
{column.id}
</DropdownMenuCheckboxItem>
);
})}
</DropdownMenuGroup>
</DropdownMenuContent>
</DropdownMenu>
</div>
<div className="overflow-hidden rounded-md border">
<Table>
<TableHeader>
{table.getHeaderGroups().map((headerGroup) => (
<TableRow key={headerGroup.id}>
{headerGroup.headers.map((header) => {
return (
<TableHead key={header.id}>
{header.isPlaceholder ? null : flexRender(header.column.columnDef.header, header.getContext())}
</TableHead>
);
})}
</TableRow>
))}
</TableHeader>
<TableBody>
{table.getRowModel().rows?.length ? (
table.getRowModel().rows.map((row) => (
<TableRow key={row.id} data-state={row.getIsSelected() && "selected"}>
{row.getVisibleCells().map((cell) => (
<TableCell key={cell.id}>{flexRender(cell.column.columnDef.cell, cell.getContext())}</TableCell>
))}
</TableRow>
))
) : (
<TableRow>
<TableCell colSpan={columns.length} className="h-24 text-center">
{t.noResults}
</TableCell>
</TableRow>
)}
</TableBody>
</Table>
</div>
<div className="flex items-center justify-end gap-2 py-4">
<div className="flex-1 text-sm text-muted-foreground">
{table.getFilteredSelectedRowModel().rows.length} {t.rowsSelected} {table.getFilteredRowModel().rows.length}{" "}
{t.rowsSelectedSuffix}
</div>
<div className="flex gap-2">
<Button
variant="outline"
size="sm"
onClick={() => table.previousPage()}
disabled={!table.getCanPreviousPage()}
>
{t.previous}
</Button>
<Button variant="outline" size="sm" onClick={() => table.nextPage()} disabled={!table.getCanNextPage()}>
{t.next}
</Button>
</div>
</div>
</div>
);
}
Anatomy
How the parts nest — every slot the component exposes, in composition order.
Table
├── TableHeader
│ └── TableRow
│ └── TableHead
└── TableBody
└── TableRow
└── TableCell
Features
- A composition recipe, not a shipped component — built entirely from @tanstack/react-table state hooks plus the plain Table primitives.
- The Demo wires sorting, column filtering, column visibility, row selection, and pagination together — each concern is independent and can be adopted separately.
Guidelines
Conventions that keep usage consistent across an app.
Do
- Drive state (sorting, filters, visibility, selection) through TanStack Table and render with the Table primitives.
- Give every interactive header and row control an accessible label.
Don’t
- Don’t paginate on the client for large datasets — fetch and sort server-side instead.
Explore further
Ready to integrate?
Follow the Getting Started guide to install @codefast/ui, or browse the full component gallery.