| Status | Amount | |||
|---|---|---|---|---|
success | $316.00 | |||
success | $242.00 | |||
processing | $837.00 | |||
success | $874.00 | |||
failed | $721.00 |
"use client";
import * as React from "react";
import { LuArrowUpDown, LuChevronDown, LuEllipsis } from "react-icons/lu";
import {
ColumnDef,
ColumnFiltersState,
flexRender,
getCoreRowModel,
getFilteredRowModel,
getPaginationRowModel,
getSortedRowModel,
SortingState,
useReactTable,
VisibilityState,
} from "@tanstack/react-table";
import { styled } from "styled-system/jsx";
import { Button } from "@/components/ui/button";
import { Checkbox } from "@/components/ui/checkbox";
import {
DropdownMenu,
DropdownMenuCheckboxItem,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuLabel,
DropdownMenuSeparator,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import { Input } from "@/components/ui/input";
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from "@/components/ui/table";
const data: Payment[] = [
{
id: "m5gr84i9",
amount: 316,
status: "success",
email: "[email protected]",
},
{
id: "3u1reuv4",
amount: 242,
status: "success",
email: "[email protected]",
},
{
id: "derv1ws0",
amount: 837,
status: "processing",
email: "[email protected]",
},
{
id: "5kma53ae",
amount: 874,
status: "success",
email: "[email protected]",
},
{
id: "bhqecj4p",
amount: 721,
status: "failed",
email: "[email protected]",
},
];
export type Payment = {
id: string;
amount: number;
status: "pending" | "processing" | "success" | "failed";
email: string;
};
export const columns: ColumnDef<Payment>[] = [
{
id: "select",
header: ({ table }) => (
<Checkbox
checked={
table.getIsAllPageRowsSelected() || (table.getIsSomePageRowsSelected() && "indeterminate")
}
onCheckedChange={(value) => table.toggleAllPageRowsSelected(!!value)}
aria-label="Select all"
/>
),
cell: ({ row }) => (
<Checkbox
checked={row.getIsSelected()}
onCheckedChange={(value) => row.toggleSelected(!!value)}
aria-label="Select row"
/>
),
enableSorting: false,
enableHiding: false,
},
{
accessorKey: "status",
header: "Status",
cell: ({ row }) => (
<styled.div css={{ textTransform: "capitalize" }}>{row.getValue("status")}</styled.div>
),
},
{
accessorKey: "email",
header: ({ column }) => {
return (
<Button
variant="ghost"
onClick={() => column.toggleSorting(column.getIsSorted() === "asc")}
>
Email
<LuArrowUpDown />
</Button>
);
},
cell: ({ row }) => (
<styled.div css={{ textTransform: "lowercase" }}>{row.getValue("email")}</styled.div>
),
},
{
accessorKey: "amount",
header: () => <styled.div css={{ textAlign: "right" }}>Amount</styled.div>,
cell: ({ row }) => {
const amount = parseFloat(row.getValue("amount"));
const formatted = new Intl.NumberFormat("en-US", {
style: "currency",
currency: "USD",
}).format(amount);
return (
<styled.div css={{ textAlign: "right", fontWeight: "medium" }}>{formatted}</styled.div>
);
},
},
{
id: "actions",
enableHiding: false,
cell: ({ row }) => {
const payment = row.original;
return (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="ghost" css={{ w: "8", h: "8", p: "0" }}>
<styled.span css={{ srOnly: true }}>Open menu</styled.span>
<LuEllipsis />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuLabel>Actions</DropdownMenuLabel>
<DropdownMenuItem onClick={() => navigator.clipboard.writeText(payment.id)}>
Copy payment ID
</DropdownMenuItem>
<DropdownMenuSeparator />
<DropdownMenuItem>View customer</DropdownMenuItem>
<DropdownMenuItem>View payment details</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
);
},
},
];
export default function DataTableDemo() {
const [sorting, setSorting] = React.useState<SortingState>([]);
const [columnFilters, setColumnFilters] = React.useState<ColumnFiltersState>([]);
const [columnVisibility, setColumnVisibility] = React.useState<VisibilityState>({});
const [rowSelection, setRowSelection] = React.useState({});
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 (
<styled.div css={{ w: "full" }}>
<styled.div css={{ display: "flex", alignItems: "center", py: "4" }}>
<Input
placeholder="Filter emails..."
value={(table.getColumn("email")?.getFilterValue() as string) ?? ""}
onChange={(event) => table.getColumn("email")?.setFilterValue(event.target.value)}
css={{ maxW: "sm" }}
/>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="outline" css={{ ml: "auto" }}>
Columns <LuChevronDown />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
{table
.getAllColumns()
.filter((column) => column.getCanHide())
.map((column) => (
<DropdownMenuCheckboxItem
key={column.id}
checked={column.getIsVisible()}
onCheckedChange={(value) => column.toggleVisibility(!!value)}
css={{ textTransform: "capitalize" }}
>
{column.id}
</DropdownMenuCheckboxItem>
))}
</DropdownMenuContent>
</DropdownMenu>
</styled.div>
<styled.div css={{ rounded: "md", borderWidth: "1px" }}>
<Table>
<TableHeader>
{table.getHeaderGroups().map((headerGroup) => (
<TableRow key={headerGroup.id}>
{headerGroup.headers.map((header) => (
<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} css={{ h: "24", textAlign: "center" }}>
No results.
</TableCell>
</TableRow>
)}
</TableBody>
</Table>
</styled.div>
<styled.div
css={{
display: "flex",
alignItems: "center",
justifyContent: "flex-end",
spaceX: "2",
py: "4",
}}
>
<styled.div css={{ flex: "1", textStyle: "sm", color: "muted.fg" }}>
{table.getFilteredSelectedRowModel().rows.length} of{" "}
{table.getFilteredRowModel().rows.length} row(s) selected.
</styled.div>
<styled.div css={{ spaceX: "2" }}>
<Button
variant="outline"
size="sm"
onClick={() => table.previousPage()}
disabled={!table.getCanPreviousPage()}
>
Previous
</Button>
<Button
variant="outline"
size="sm"
onClick={() => table.nextPage()}
disabled={!table.getCanNextPage()}
>
Next
</Button>
</styled.div>
</styled.div>
</styled.div>
);
}
Introduction
Every data table or datagrid I've created has been unique. They all behave differently, have specific sorting and filtering requirements, and work with different data sources.
It doesn't make sense to combine all of these variations into a single component. If we do that, we'll lose the flexibility that headless UI provides.
So instead of a data table component, I thought it would be more helpful to provide a guide on how to build your own.
We'll start with the basic <Table /> component and build a complex data table from scratch.
Table of Contents
This guide will show you how to use TanStack Table and the
<Table /> component to build your own custom data table. We'll cover the following topics:
Installation
Add the Table component to your project.
Add @tanstack/react-table as a dependency to your project.
npm install @tanstack/react-tablePrerequisites
We are going to build a table to show recent payments. Here's what our data looks like:
type Payment = {
id: string;
amount: number;
status: "pending" | "processing" | "success" | "failed";
email: string;
};
export const payments: Payment[] = [
{
id: "728ed52f",
amount: 100,
status: "pending",
email: "[email protected]",
},
{
id: "489e1d42",
amount: 125,
status: "processing",
email: "[email protected]",
},
// ...
];
Project Structure
Start by creating the following project structure:
I'm using a Next.js example here but this works for any other React framework.
columns.tsx(client component) will contain our column definitions.data-table.tsx(client component) will contain our<DataTable />component.page.tsx(server component) is where we'll fetch data and render our table.
Basic Table
Let's start by building a basic table.
Column Definitions
First we'll define our columns.
app/payments/columns.tsx
"use client";
import { ColumnDef } from "@tanstack/react-table";
// This type is used to define the shape of our data.
// You can use a Zod schema here if you want.
export type Payment = {
id: string;
amount: number;
status: "pending" | "processing" | "success" | "failed";
email: string;
};
export const columns: ColumnDef<Payment>[] = [
{
accessorKey: "status",
header: "Status",
},
{
accessorKey: "email",
header: "Email",
},
{
accessorKey: "amount",
header: "Amount",
},
];
Note: Columns are where you define the core of what your table will look like. They define the data that will be displayed, how it will be formatted, sorted and filtered.
<DataTable /> component
Next, we'll create a <DataTable /> component to render our table.
app/payments/data-table.tsx
"use client";
import { ColumnDef, flexRender, getCoreRowModel, useReactTable } from "@tanstack/react-table";
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from "@/components/ui/table";
interface DataTableProps<TData, TValue> {
columns: ColumnDef<TData, TValue>[];
data: TData[];
}
export default function DataTable<TData, TValue>({ columns, data }: DataTableProps<TData, TValue>) {
const table = useReactTable({
data,
columns,
getCoreRowModel: getCoreRowModel(),
});
return (
<styled.div css={{ rounded: "md", borderWidth: "1px" }}>
<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} css={{ h: "24", textAlign: "center" }}>
No results.
</TableCell>
</TableRow>
)}
</TableBody>
</Table>
</styled.div>
);
}
Tip: If you find yourself using in multiple places, this is the component you
could make reusable by extracting it to components/ui/data-table.tsx.
<DataTable columns={columns} data={data} />
Render the table
Finally, we'll render our table in our page component.
app/payments/page.tsx
import { columns, Payment } from "./columns";
import { DataTable } from "./data-table";
async function getData(): Promise<Payment[]> {
// Fetch data from your API here.
return [
{
id: "728ed52f",
amount: 100,
status: "pending",
email: "[email protected]",
},
// ...
];
}
export default async function DemoPage() {
const data = await getData();
return (
<styled.div className={container()}>
<DataTable columns={columns} data={data} />
</styled.div>
);
}
Cell Formatting
Let's format the amount cell to display the dollar amount. We'll also align the cell to the right.
Update columns definition
Update the header and cell definitions for amount as follows:
app/payments/columns.tsx
export const columns: ColumnDef<Payment>[] = [
{
accessorKey: "amount",
header: () => <styled.div css={{ textAlign: "right" }}>Amount</styled.div>,
cell: ({ row }) => {
const amount = parseFloat(row.getValue("amount"));
const formatted = new Intl.NumberFormat("en-US", {
style: "currency",
currency: "USD",
}).format(amount);
return (
<styled.div css={{ textAlign: "right", fontWeight: "medium" }}>{formatted}</styled.div>
);
},
},
];
You can use the same approach to format other cells and headers.
Row Actions
Let's add row actions to our table. We'll use a <Dropdown /> component for this.
Update columns definition
Update our columns definition to add a new actions column. The actions cell returns a
<Dropdown /> component.
app/payments/columns.tsx
"use client";
import { LuEllipsis } from "react-icons/lu";
import { ColumnDef } from "@tanstack/react-table";
import { css } from "styled-system/css";
import { Button } from "@/components/ui/button";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuLabel,
DropdownMenuSeparator,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
export const columns: ColumnDef<Payment>[] = [
// ...
{
id: "actions",
cell: ({ row }) => {
const payment = row.original;
return (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="ghost" css={{ w: "8", h: "8", p: "0" }}>
<styled.span css={{ srOnly: true }}>Open menu</styled.span>
<LuEllipsis className={css({ w: "4", h: "4" })} />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuLabel>Actions</DropdownMenuLabel>
<DropdownMenuItem onClick={() => navigator.clipboard.writeText(payment.id)}>
Copy payment ID
</DropdownMenuItem>
<DropdownMenuSeparator />
<DropdownMenuItem>View customer</DropdownMenuItem>
<DropdownMenuItem>View payment details</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
);
},
},
// ...
];
You can access the row data using row.original in the cell function. Use this to handle actions
for your row eg. use the id to make a DELETE call to your API.
Pagination
Next, we'll add pagination to our table.
Update the <DataTable />
app/payments/data-table.tsx
import {
ColumnDef,
flexRender,
getCoreRowModel,
getPaginationRowModel,
useReactTable,
} from "@tanstack/react-table";
export function DataTable<TData, TValue>({ columns, data }: DataTableProps<TData, TValue>) {
const table = useReactTable({
data,
columns,
getCoreRowModel: getCoreRowModel(),
getPaginationRowModel: getPaginationRowModel(),
});
// ...
}
This will automatically paginate your rows into pages of 10. See the pagination docs for more information on customizing page size and implementing manual pagination.
Add pagination controls
We can add pagination controls to our table using the <Button /> component and the
table.previousPage(), table.nextPage() API methods.
app/payments/data-table.tsx
import { Button } from "@/components/ui/button";
export function DataTable<TData, TValue>({ columns, data }: DataTableProps<TData, TValue>) {
const table = useReactTable({
data,
columns,
getCoreRowModel: getCoreRowModel(),
getPaginationRowModel: getPaginationRowModel(),
});
return (
<styled.div>
<styled.div css={{ rounded: "md", borderWidth: "1px" }}>
<Table>
{ // .... }
</Table>
</styled.div>
<styled.div
css={{
display: "flex",
alignItems: "center",
justifyContent: "flex-end",
spaceX: "2",
py: "4",
}}
>
<Button
variant="outline"
size="sm"
onClick={() => table.previousPage()}
disabled={!table.getCanPreviousPage()}
>
Previous
</Button>
<Button
variant="outline"
size="sm"
onClick={() => table.nextPage()}
disabled={!table.getCanNextPage()}
>
Next
</Button>
</styled.div>
</styled.div>
);
}
See Reusable Components section for a more advanced pagination component.
Sorting
Let's make the email column sortable.
Update <DataTable />
app/payments/data-table.tsx
"use client"
import * as React from "react"
import {
ColumnDef,
SortingState,
flexRender,
getCoreRowModel,
getPaginationRowModel,
getSortedRowModel,
useReactTable,
} from "@tanstack/react-table"
export function DataTable<TData, TValue>({
columns,
data,
}: DataTableProps<TData, TValue>) {
const [sorting, setSorting] = React.useState<SortingState>([])
const table = useReactTable({
data,
columns,
getCoreRowModel: getCoreRowModel(),
getPaginationRowModel: getPaginationRowModel(),
onSortingChange: setSorting,
getSortedRowModel: getSortedRowModel(),
state: {
sorting,
},
})
return (
<styled.div>
<styled.div css={{ rounded: "md", borderWidth: "1px" }}>
<Table>{ ... }</Table>
</styled.div>
</styled.div>
)
}
Make header cell sortable
We can now update the email header cell to add sorting controls.
app/payments/columns.tsx
"use client";
import { LuArrowUpDown } from "react-icons/lu";
import { ColumnDef } from "@tanstack/react-table";
export const columns: ColumnDef<Payment>[] = [
{
accessorKey: "email",
header: ({ column }) => {
return (
<Button
variant="ghost"
onClick={() => column.toggleSorting(column.getIsSorted() === "asc")}
>
Email
<LuArrowUpDown className={css({ ml: "2", w: "4", h: "4" })} />
</Button>
);
},
},
];
This will automatically sort the table (asc and desc) when the user toggles on the header cell.
Filtering
Let's add a search input to filter emails in our table.
Update <DataTable />
app/payments/data-table.tsx
"use client"
import * as React from "react"
import {
ColumnDef,
ColumnFiltersState,
SortingState,
flexRender,
getCoreRowModel,
getFilteredRowModel,
getPaginationRowModel,
getSortedRowModel,
useReactTable,
} from "@tanstack/react-table"
import { Button } from "@/components/ui/button"
import { Input } from "@/components/ui/input"
export function DataTable<TData, TValue>({
columns,
data,
}: DataTableProps<TData, TValue>) {
const [sorting, setSorting] = React.useState<SortingState>([])
const [columnFilters, setColumnFilters] = React.useState<ColumnFiltersState>(
[]
)
const table = useReactTable({
data,
columns,
onSortingChange: setSorting,
getCoreRowModel: getCoreRowModel(),
getPaginationRowModel: getPaginationRowModel(),
getSortedRowModel: getSortedRowModel(),
onColumnFiltersChange: setColumnFilters,
getFilteredRowModel: getFilteredRowModel(),
state: {
sorting,
columnFilters,
},
})
return (
<styled.div>
<styled.div css={{ display: "flex", alignItems: "center", py: "4" }}>
<Input
placeholder="Filter emails..."
value={(table.getColumn("email")?.getFilterValue() as string) ?? ""}
onChange={(event) => table.getColumn("email")?.setFilterValue(event.target.value)}
css={{ maxW: "sm" }}
/>
</styled.div>
<styled.div css={{ rounded: "md", borderWidth: "1px" }}>
<Table>{ ... }</Table>
</styled.div>
</styled.div>
)
}
Filtering is now enabled for the email column. You can add filters to other columns as well.
See the filtering docs for more information on customizing filters.
Visibility
Adding column visibility is fairly simple using @tanstack/react-table visibility API.
Update <DataTable />
app/payments/data-table.tsx
"use client"
import * as React from "react"
import {
ColumnDef,
ColumnFiltersState,
SortingState,
VisibilityState,
flexRender,
getCoreRowModel,
getFilteredRowModel,
getPaginationRowModel,
getSortedRowModel,
useReactTable,
} from "@tanstack/react-table"
import { Button } from "@/components/ui/button"
import {
DropdownMenu,
DropdownMenuCheckboxItem,
DropdownMenuContent,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu"
export function DataTable<TData, TValue>({
columns,
data,
}: DataTableProps<TData, TValue>) {
const [sorting, setSorting] = React.useState<SortingState>([])
const [columnFilters, setColumnFilters] = React.useState<ColumnFiltersState>(
[]
)
const [columnVisibility, setColumnVisibility] =
React.useState<VisibilityState>({})
const table = useReactTable({
data,
columns,
onSortingChange: setSorting,
onColumnFiltersChange: setColumnFilters,
getCoreRowModel: getCoreRowModel(),
getPaginationRowModel: getPaginationRowModel(),
getSortedRowModel: getSortedRowModel(),
getFilteredRowModel: getFilteredRowModel(),
onColumnVisibilityChange: setColumnVisibility,
state: {
sorting,
columnFilters,
columnVisibility,
},
})
return (
<styled.div>
<styled.div css={{ display: "flex", alignItems: "center", py: "4" }}>
<Input
placeholder="Filter emails..."
value={table.getColumn("email")?.getFilterValue() as string}
onChange={(event) =>
table.getColumn("email")?.setFilterValue(event.target.value)
}
css={{ maxW: "sm" }}
/>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="outline" css={{ ml: "auto" }}>
Columns
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
{table
.getAllColumns()
.filter(
(column) => column.getCanHide()
)
.map((column) => {
return (
<DropdownMenuCheckboxItem
key={column.id}
css={{ textTransform: "capitalize" }}
checked={column.getIsVisible()}
onCheckedChange={(value) =>
column.toggleVisibility(!!value)
}
>
{column.id}
</DropdownMenuCheckboxItem>
)
})}
</DropdownMenuContent>
</DropdownMenu>
</styled.div>
<styled.div css={{ rounded: "md", borderWidth: "1px" }}>
<Table>{ ... }</Table>
</styled.div>
</styled.div>
)
}
This adds a dropdown menu that you can use to toggle column visibility.
Row Selection
Next, we're going to add row selection to our table.
Update columns definition
app/payments/columns.tsx
"use client";
import { ColumnDef } from "@tanstack/react-table";
import { Badge } from "@/components/ui/badge";
import { Checkbox } from "@/components/ui/checkbox";
export const columns: ColumnDef<Payment>[] = [
{
id: "select",
header: ({ table }) => (
<Checkbox
checked={
table.getIsAllPageRowsSelected() || (table.getIsSomePageRowsSelected() && "indeterminate")
}
onCheckedChange={(value) => table.toggleAllPageRowsSelected(!!value)}
aria-label="Select all"
/>
),
cell: ({ row }) => (
<Checkbox
checked={row.getIsSelected()}
onCheckedChange={(value) => row.toggleSelected(!!value)}
aria-label="Select row"
/>
),
enableSorting: false,
enableHiding: false,
},
];
Update <DataTable />
app/payments/data-table.tsx
export function DataTable<TData, TValue>({ columns, data }: DataTableProps<TData, TValue>) {
const [sorting, setSorting] = React.useState<SortingState>([]);
const [columnFilters, setColumnFilters] = React.useState<ColumnFiltersState>([]);
const [columnVisibility, setColumnVisibility] = React.useState<VisibilityState>({});
const [rowSelection, setRowSelection] = React.useState({});
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 (
<styled.div>
<styled.div css={{ rounded: "md", borderWidth: "1px" }}>
<Table />
</styled.div>
</styled.div>
);
}
This adds a checkbox to each row and a checkbox in the header to select all rows.
Show selected rows
You can show the number of selected rows using the table.getFilteredSelectedRowModel() API.
<styled.div css={{ flex: "1", textStyle: "sm", color: "muted.fg" }}>
{table.getFilteredSelectedRowModel().rows.length} of {table.getFilteredRowModel().rows.length}{" "}
row(s) selected.
</styled.div>
Reusable Components
Column Header
Make any column header sortable and hideable.
import { LuArrowDown, LuArrowUp, LuChevronsUpDown, LuEyeOff } from "react-icons/lu";
import { Column } from "@tanstack/react-table";
import { css } from "styled-system/css";
import { styled } from "styled-system/jsx";
import { Button } from "@/components/ui/button";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuSeparator,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
interface DataTableColumnHeaderProps<TData, TValue>
extends React.ComponentProps<typeof styled.div> {
column: Column<TData, TValue>;
title: string;
}
export function DataTableColumnHeader<TData, TValue>({
column,
title,
css,
className,
}: DataTableColumnHeaderProps<TData, TValue>) {
if (!column.getCanSort()) {
return <styled.div className={className}>{title}</styled.div>;
}
return (
<styled.div
className={className}
css={{ display: "flex", alignItems: "center", spaceX: "2", ...css }}
>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="ghost" size="sm" css={{ ml: "3", h: "8", _open: { bg: "accent" } }}>
<span>{title}</span>
{column.getIsSorted() === "desc" ? (
<LuArrowDown />
) : column.getIsSorted() === "asc" ? (
<LuArrowUp />
) : (
<LuChevronsUpDown />
)}
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="start">
<DropdownMenuItem onClick={() => column.toggleSorting(false)}>
<LuArrowUp className={css({ w: "3.5", h: "3.5", color: "muted.fg/70" })} />
Asc
</DropdownMenuItem>
<DropdownMenuItem onClick={() => column.toggleSorting(true)}>
<LuArrowDown className={css({ w: "3.5", h: "3.5", color: "muted.fg/70" })} />
Desc
</DropdownMenuItem>
<DropdownMenuSeparator />
<DropdownMenuItem onClick={() => column.toggleVisibility(false)}>
<LuEyeOff className={css({ w: "3.5", h: "3.5", color: "muted.fg/70" })} />
Hide
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</styled.div>
);
}
export const columns = [
{
accessorKey: "email",
header: ({ column }) => <DataTableColumnHeader column={column} title="Email" />,
},
];
Pagination Component
import { LuChevronLeft, LuChevronRight, LuChevronsLeft, LuChevronsRight } from "react-icons/lu";
import { Table } from "@tanstack/react-table";
import { css } from "styled-system/css";
import { styled } from "styled-system/jsx";
import { Button } from "@/components/ui/button";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
interface DataTablePaginationProps<TData> {
table: Table<TData>;
}
export function DataTablePagination<TData>({ table }: DataTablePaginationProps<TData>) {
return (
<styled.div
css={{ display: "flex", alignItems: "center", justifyContent: "space-between", px: "2" }}
>
<styled.div css={{ flex: "1", textStyle: "sm", color: "muted.fg" }}>
{table.getFilteredSelectedRowModel().rows.length} of{" "}
{table.getFilteredRowModel().rows.length} row(s) selected.
</styled.div>
<styled.div css={{ display: "flex", alignItems: "center", spaceX: "6", lg: { spaceX: "8" } }}>
<styled.div css={{ display: "flex", alignItems: "center", spaceX: "2" }}>
<styled.p css={{ textStyle: "sm", fontWeight: "medium" }}>Rows per page</styled.p>
<Select
value={`${table.getState().pagination.pageSize}`}
onValueChange={(value) => {
table.setPageSize(Number(value));
}}
>
<SelectTrigger css={{ w: "70px", h: "8" }}>
<SelectValue placeholder={table.getState().pagination.pageSize} />
</SelectTrigger>
<SelectContent side="top">
{[10, 20, 30, 40, 50].map((pageSize) => (
<SelectItem key={pageSize} value={`${pageSize}`}>
{pageSize}
</SelectItem>
))}
</SelectContent>
</Select>
</styled.div>
<styled.div
css={{
display: "flex",
w: "100px",
alignItems: "center",
justifyContent: "center",
textStyle: "sm",
fontWeight: "medium",
}}
>
Page {table.getState().pagination.pageIndex + 1} of {table.getPageCount()}
</styled.div>
<styled.div css={{ display: "flex", alignItems: "center", spaceX: "2" }}>
<Button
variant="outline"
css={{ display: "none", w: "8", h: "8", lg: { display: "flex" } }}
onClick={() => table.setPageIndex(0)}
disabled={!table.getCanPreviousPage()}
>
<styled.span css={{ srOnly: true }}>Go to first page</styled.span>
<LuChevronsLeft />
</Button>
<Button
variant="outline"
css={{ w: "8", h: "8", p: "0" }}
onClick={() => table.previousPage()}
disabled={!table.getCanPreviousPage()}
>
<styled.span css={{ srOnly: true }}>Go to previous page</styled.span>
<LuChevronLeft />
</Button>
<Button
variant="outline"
css={{ w: "8", h: "8", p: "0" }}
onClick={() => table.nextPage()}
disabled={!table.getCanNextPage()}
>
<styled.span css={{ srOnly: true }}>Go to next page</styled.span>
<LuChevronRight />
</Button>
<Button
variant="outline"
css={{ display: "none", w: "8", h: "8", lg: { display: "flex" } }}
onClick={() => table.setPageIndex(table.getPageCount() - 1)}
disabled={!table.getCanNextPage()}
>
<styled.span css={{ srOnly: true }}>Go to last page</styled.span>
<LuChevronsRight />
</Button>
</styled.div>
</styled.div>
</styled.div>
);
}
<DataTablePagination table={table} />
Column Toggle
A component to toggle column visibility.
"use client";
import { LuSettings2 } from "react-icons/lu";
import { Table } from "@tanstack/react-table";
import { Button } from "@/components/ui/button";
import {
DropdownMenu,
DropdownMenuCheckboxItem,
DropdownMenuContent,
DropdownMenuLabel,
DropdownMenuSeparator,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
interface DataTableViewOptionsProps<TData> {
table: Table<TData>;
}
export function DataTableViewOptions<TData>({ table }: DataTableViewOptionsProps<TData>) {
return (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button
variant="outline"
size="sm"
css={{ ml: "auto", display: "none", h: "8", lg: { display: "flex" } }}
>
<LuSettings2 />
View
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end" css={{ w: "150px" }}>
<DropdownMenuLabel>Toggle columns</DropdownMenuLabel>
<DropdownMenuSeparator />
{table
.getAllColumns()
.filter((column) => typeof column.accessorFn !== "undefined" && column.getCanHide())
.map((column) => {
return (
<DropdownMenuCheckboxItem
key={column.id}
css={{ textTransform: "capitalize" }}
checked={column.getIsVisible()}
onCheckedChange={(value) => column.toggleVisibility(!!value)}
>
{column.id}
</DropdownMenuCheckboxItem>
);
})}
</DropdownMenuContent>
</DropdownMenu>
);
}
<DataTableViewOptions table={table} />