All components
Table With Ellipsis Pagination
tablesOrigin UI component.
responsive · 600px
Install
Same command in any shadcn project — React (Vite/CRA), Next.js, Remix, Astro, and more:
$
npx shadcn@latest add https://your-domain/r/comp-484.jsonUsage
import Cmp from "@/registry/origin-ui/comp-484";
export default function Demo() {
return <Cmp />;
}Component source
"use client";
import {
type ColumnDef,
flexRender,
getCoreRowModel,
getPaginationRowModel,
getSortedRowModel,
type PaginationState,
type SortingState,
useReactTable,
} from "@tanstack/react-table";
import {
ChevronDownIcon,
ChevronLeftIcon,
ChevronRightIcon,
ChevronUpIcon,
} from "lucide-react";
import { useEffect, useState } from "react";
import { usePagination } from "@/registry/origin-ui/use-pagination";
import { cn } from "@/lib/utils";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import { Checkbox } from "@/components/ui/checkbox";
import {
Pagination,
PaginationContent,
PaginationEllipsis,
PaginationItem,
} from "@/components/ui/pagination";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from "@/components/ui/table";
type Item = {
id: string;
name: string;
email: string;
location: string;
flag: string;
status: "Active" | "Inactive" | "Pending";
balance: number;
};
const columns: ColumnDef<Item>[] = [
{
cell: ({ row }) => (
<Checkbox
aria-label="Select row"
checked={row.getIsSelected()}
onCheckedChange={(value) => row.toggleSelected(!!value)}
/>
),
enableSorting: false,
header: ({ table }) => (
<Checkbox
aria-label="Select all rows"
checked={
table.getIsAllPageRowsSelected() ||
(table.getIsSomePageRowsSelected() && "indeterminate")
}
onCheckedChange={(value) => table.toggleAllPageRowsSelected(!!value)}
/>
),
id: "select",
size: 28,
},
{
accessorKey: "name",
cell: ({ row }) => (
<div className="font-medium">{row.getValue("name")}</div>
),
header: "Name",
size: 180,
},
{
accessorKey: "email",
header: "Email",
size: 200,
},
{
accessorKey: "location",
cell: ({ row }) => (
<div>
<span className="text-lg leading-none">{row.original.flag}</span>{" "}
{row.getValue("location")}
</div>
),
header: "Location",
size: 180,
},
{
accessorKey: "status",
cell: ({ row }) => (
<Badge
className={cn(
row.getValue("status") === "Inactive" &&
"bg-muted-foreground/60 text-primary-foreground",
)}
>
{row.getValue("status")}
</Badge>
),
header: "Status",
size: 120,
},
{
accessorKey: "balance",
cell: ({ row }) => {
const amount = Number.parseFloat(row.getValue("balance"));
const formatted = new Intl.NumberFormat("en-US", {
currency: "USD",
style: "currency",
}).format(amount);
return formatted;
},
header: "Balance",
size: 120,
},
];
export default function Component() {
const pageSize = 5;
const [pagination, setPagination] = useState<PaginationState>({
pageIndex: 0,
pageSize: pageSize,
});
const [sorting, setSorting] = useState<SortingState>([
{
desc: false,
id: "name",
},
]);
const [data, setData] = useState<Item[]>([]);
useEffect(() => {
async function fetchPosts() {
const res = await fetch(
"https://raw.githubusercontent.com/origin-space/origin-images/refs/heads/main/users-01_fertyx.json",
);
const data = await res.json();
setData(data);
}
fetchPosts();
}, []);
const table = useReactTable({
columns,
data,
enableSortingRemoval: false,
getCoreRowModel: getCoreRowModel(),
getPaginationRowModel: getPaginationRowModel(),
getSortedRowModel: getSortedRowModel(),
onPaginationChange: setPagination,
onSortingChange: setSorting,
state: {
pagination,
sorting,
},
});
const { pages, showLeftEllipsis, showRightEllipsis } = usePagination({
currentPage: table.getState().pagination.pageIndex + 1,
paginationItemsToDisplay: 5,
totalPages: table.getPageCount(),
});
return (
<div className="space-y-4">
<div className="overflow-hidden rounded-md border bg-background">
<Table className="table-fixed">
<TableHeader>
{table.getHeaderGroups().map((headerGroup) => (
<TableRow className="hover:bg-transparent" key={headerGroup.id}>
{headerGroup.headers.map((header) => {
return (
<TableHead
className="h-11"
key={header.id}
style={{ width: `${header.getSize()}px` }}
>
{header.isPlaceholder ? null : header.column.getCanSort() ? (
<div
className={cn(
header.column.getCanSort() &&
"flex h-full cursor-pointer select-none items-center justify-between gap-2",
)}
onClick={header.column.getToggleSortingHandler()}
onKeyDown={(e) => {
// Enhanced keyboard handling for sorting
if (
header.column.getCanSort() &&
(e.key === "Enter" || e.key === " ")
) {
e.preventDefault();
header.column.getToggleSortingHandler()?.(e);
}
}}
tabIndex={header.column.getCanSort() ? 0 : undefined}
>
{flexRender(
header.column.columnDef.header,
header.getContext(),
)}
{{
asc: (
<ChevronUpIcon
aria-hidden="true"
className="shrink-0 opacity-60"
size={16}
/>
),
desc: (
<ChevronDownIcon
aria-hidden="true"
className="shrink-0 opacity-60"
size={16}
/>
),
}[header.column.getIsSorted() as string] ?? null}
</div>
) : (
flexRender(
header.column.columnDef.header,
header.getContext(),
)
)}
</TableHead>
);
})}
</TableRow>
))}
</TableHeader>
<TableBody>
{table.getRowModel().rows?.length ? (
table.getRowModel().rows.map((row) => (
<TableRow
data-state={row.getIsSelected() && "selected"}
key={row.id}
>
{row.getVisibleCells().map((cell) => (
<TableCell key={cell.id}>
{flexRender(
cell.column.columnDef.cell,
cell.getContext(),
)}
</TableCell>
))}
</TableRow>
))
) : (
<TableRow>
<TableCell
className="h-24 text-center"
colSpan={columns.length}
>
No results.
</TableCell>
</TableRow>
)}
</TableBody>
</Table>
</div>
{/* Pagination */}
<div className="flex items-center justify-between gap-3 max-sm:flex-col">
{/* Page number information */}
<p
aria-live="polite"
className="flex-1 whitespace-nowrap text-muted-foreground text-sm"
>
Page{" "}
<span className="text-foreground">
{table.getState().pagination.pageIndex + 1}
</span>{" "}
of <span className="text-foreground">{table.getPageCount()}</span>
</p>
{/* Pagination buttons */}
<div className="grow">
<Pagination>
<PaginationContent>
{/* Previous page button */}
<PaginationItem>
<Button
aria-label="Go to previous page"
className="disabled:pointer-events-none disabled:opacity-50"
disabled={!table.getCanPreviousPage()}
onClick={() => table.previousPage()}
size="icon"
variant="outline"
>
<ChevronLeftIcon aria-hidden="true" size={16} />
</Button>
</PaginationItem>
{/* Left ellipsis (...) */}
{showLeftEllipsis && (
<PaginationItem>
<PaginationEllipsis />
</PaginationItem>
)}
{/* Page number buttons */}
{pages.map((page) => {
const isActive =
page === table.getState().pagination.pageIndex + 1;
return (
<PaginationItem key={page}>
<Button
aria-current={isActive ? "page" : undefined}
onClick={() => table.setPageIndex(page - 1)}
size="icon"
variant={`${isActive ? "outline" : "ghost"}`}
>
{page}
</Button>
</PaginationItem>
);
})}
{/* Right ellipsis (...) */}
{showRightEllipsis && (
<PaginationItem>
<PaginationEllipsis />
</PaginationItem>
)}
{/* Next page button */}
<PaginationItem>
<Button
aria-label="Go to next page"
className="disabled:pointer-events-none disabled:opacity-50"
disabled={!table.getCanNextPage()}
onClick={() => table.nextPage()}
size="icon"
variant="outline"
>
<ChevronRightIcon aria-hidden="true" size={16} />
</Button>
</PaginationItem>
</PaginationContent>
</Pagination>
</div>
{/* Results per page */}
<div className="flex flex-1 justify-end">
<Select
aria-label="Results per page"
onValueChange={(value) => {
table.setPageSize(Number(value));
}}
value={table.getState().pagination.pageSize.toString()}
>
<SelectTrigger
className="w-fit whitespace-nowrap"
id="results-per-page"
>
<SelectValue placeholder="Select number of results" />
</SelectTrigger>
<SelectContent>
{[5, 10, 25, 50].map((pageSize) => (
<SelectItem key={pageSize} value={pageSize.toString()}>
{pageSize} / page
</SelectItem>
))}
</SelectContent>
</Select>
</div>
</div>
<p className="mt-4 text-center text-muted-foreground text-sm">
Numeric pagination made with{" "}
<a
className="underline hover:text-foreground"
href="https://tanstack.com/table"
rel="noopener noreferrer"
target="_blank"
>
TanStack Table
</a>
</p>
</div>
);
}Dependencies
@tanstack/react-table
Source: Origin UI