Datentabelle
Sortierbare, filterbare und paginierte Tabellen für Admin-Bereiche, Dashboards und Bestelllisten – mit dem „Neobrutalismus“-Stil aus Rahmen und Schatten.
Die Data Table ist eine Anleitung, keine fertige Komponente: Du komponierst deine eigene Tabelle aus der <Table />-Komponente und TanStack Table, einer Headless-Engine für Sortierung, Filterung, Paginierung und Zeilenauswahl – die Optik übernimmt das neobrutalistische Rezept aus dicken Rahmen, harten Schatten und kräftiger Typografie.
Die richtige Wahl für:
- Admin-Panels – Bestellungen, Nutzer und Rechnungen mit sortierbaren Spalten, Filtern und Aktionsmenüs pro Zeile.
- Dashboards – Zahlungs- oder Aktivitätstabellen mit formatierten Zellen, Statusspalten und paginierten API-Daten.
- Massen-Workflows – Zeilenauswahl per Checkbox, um viele Datensätze auf einmal zu exportieren, zu archivieren oder zu löschen.
Einführung#
Jede Datentabelle oder jedes Datagrid, das ich erstellt habe, war einzigartig. Sie verhalten sich alle unterschiedlich, haben spezifische Sortier- und Filteranforderungen und arbeiten mit unterschiedlichen Datenquellen.
Es ergibt keinen Sinn, all diese Variationen in einer einzigen Komponente zu vereinen. Wenn wir das tun, verlieren wir die Flexibilität, die Headless UI bietet.
Statt einer Data-Table-Komponente hielt ich es daher für hilfreicher, eine Anleitung bereitzustellen, wie du deine eigene erstellst.
Wir beginnen mit der grundlegenden <Table />-Komponente und bauen eine komplexe Datentabelle von Grund auf.
Tipp: Wenn du dieselbe Tabelle an mehreren Stellen in deiner App verwendest, kannst du sie jederzeit in eine wiederverwendbare Komponente auslagern.
Inhaltsverzeichnis#
Diese Anleitung zeigt dir, wie du TanStack Table und die <Table />-Komponente verwendest, um deine eigene benutzerdefinierte Datentabelle zu erstellen. Wir behandeln die folgenden Themen:
- Grundlegende Tabelle
- Zeilenaktionen
- Paginierung
- Sortierung
- Filterung
- Sichtbarkeit
- Zeilenauswahl
- Wiederverwendbare Komponenten
Installation#
- Füge die
<Table />-Komponente zu deinem Projekt hinzu:
pnpm dlx shadcn@latest add tablenpx shadcn@latest add tableyarn dlx shadcn@latest add tablebunx --bun shadcn@latest add table
- Füge die Abhängigkeit
tanstack/react-tablehinzu:
pnpm add @tanstack/react-tablenpm install @tanstack/react-tableyarn add @tanstack/react-tablebun add @tanstack/react-table
Voraussetzungen#
Wir werden eine Tabelle erstellen, um aktuelle Zahlungen anzuzeigen. So sehen unsere Daten aus:
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]",
},
// ...
]Projektstruktur#
Erstelle zunächst die folgende Dateistruktur:
app
└── payments
├── columns.tsx
├── data-table.tsx
└── page.tsxIch verwende hier ein Next.js-Beispiel, aber das funktioniert mit jedem anderen React-Framework.
columns.tsx(Client-Komponente) enthält unsere Spaltendefinitionen.data-table.tsx(Client-Komponente) enthält unsere<DataTable />-Komponente.page.tsx(Server-Komponente) ist der Ort, an dem wir Daten abrufen und unsere Tabelle rendern.
Grundlegende Tabelle#
Beginnen wir mit dem Erstellen einer grundlegenden Tabelle.
Spaltendefinitionen#
Zuerst definieren wir unsere Spalten.
"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",
},
]Hinweis: In den Spalten definierst du den Kern des Aussehens deiner Tabelle. Sie legen fest, welche Daten angezeigt werden und wie sie formatiert, sortiert und gefiltert werden.
<DataTable />-Komponente#
Als Nächstes erstellen wir eine <DataTable />-Komponente, um unsere Tabelle zu rendern.
"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 function DataTable<TData, TValue>({
columns,
data,
}: DataTableProps<TData, TValue>) {
const table = useReactTable({
data,
columns,
getCoreRowModel: getCoreRowModel(),
})
return (
<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">
No results.
</TableCell>
</TableRow>
)}
</TableBody>
</Table>
</div>
)
}Tipp: Wenn du <DataTable /> an mehreren Stellen verwendest, ist dies die Komponente, die du wiederverwendbar machen könntest, indem du sie in components/ui/data-table.tsx auslagerst.
<DataTable columns={columns} data={data} />
Die Tabelle rendern#
Schließlich rendern wir unsere Tabelle in unserer Seitenkomponente.
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 (
<div className="container mx-auto py-10">
<DataTable columns={columns} data={data} />
</div>
)
}Zellenformatierung#
Formatieren wir die Betragszelle, um den Dollarbetrag anzuzeigen. Außerdem richten wir die Zelle rechtsbündig aus.
Spaltendefinition aktualisieren#
Aktualisiere die header- und cell-Definitionen für den Betrag wie folgt:
export const columns: ColumnDef<Payment>[] = [
{
accessorKey: "amount",
header: () => <div className="text-right">Amount</div>,
cell: ({ row }) => {
const amount = parseFloat(row.getValue("amount"))
const formatted = new Intl.NumberFormat("en-US", {
style: "currency",
currency: "USD",
}).format(amount)
return <div className="text-right font-medium">{formatted}</div>
},
},
]Du kannst denselben Ansatz verwenden, um andere Zellen und Header zu formatieren.
Zeilenaktionen#
Fügen wir unserer Tabelle Zeilenaktionen hinzu. Dafür verwenden wir eine <DropdownMenu />-Komponente.
Spaltendefinition aktualisieren#
Aktualisiere unsere Spaltendefinition, um eine neue actions-Spalte hinzuzufügen. Die actions-Zelle gibt eine <DropdownMenu />-Komponente zurück.
"use client"
import { ColumnDef } from "@tanstack/react-table"
import { MoreHorizontal } from "lucide-react"
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" className="h-8 w-8 p-0">
<span className="sr-only">Open menu</span>
<MoreHorizontal className="h-4 w-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>
)
},
},
// ...
]Du kannst über row.original in der cell-Funktion auf die Zeilendaten zugreifen. Verwende dies, um Aktionen für deine Zeile zu verarbeiten, z. B. verwende die id, um einen DELETE-Aufruf an deine API zu senden.
Paginierung#
Als Nächstes fügen wir unserer Tabelle Paginierung hinzu.
<DataTable> aktualisieren#
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(),
})
// ...
}Dadurch werden deine Zeilen automatisch in Seiten zu je 10 aufgeteilt. Weitere Informationen zum Anpassen der Seitengröße und zum Implementieren manueller Paginierung findest du in der Paginierungs-Dokumentation.
Paginierungssteuerelemente hinzufügen#
Wir können unserer Tabelle Paginierungssteuerelemente mithilfe der <Button />-Komponente und der API-Methoden table.previousPage(), table.nextPage() hinzufügen.
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 (
<div>
<div className="overflow-hidden rounded-md border">
<Table>
{ // .... }
</Table>
</div>
<div className="flex items-center justify-end space-x-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>
</div>
</div>
)
}Im Abschnitt Wiederverwendbare Komponenten findest du eine fortgeschrittenere Paginierungskomponente.
Sortierung#
Machen wir die E-Mail-Spalte sortierbar.
<DataTable> aktualisieren#
"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 (
<div>
<div className="overflow-hidden rounded-md border">
<Table>{ ... }</Table>
</div>
</div>
)
}Header-Zelle sortierbar machen#
Wir können jetzt die email-Header-Zelle aktualisieren, um Sortiersteuerelemente hinzuzufügen.
"use client"
import { ColumnDef } from "@tanstack/react-table"
import { ArrowUpDown } from "lucide-react"
export const columns: ColumnDef<Payment>[] = [
{
accessorKey: "email",
header: ({ column }) => {
return (
<Button
variant="ghost"
onClick={() => column.toggleSorting(column.getIsSorted() === "asc")}
>
Email
<ArrowUpDown className="ml-2 h-4 w-4" />
</Button>
)
},
},
]Dadurch wird die Tabelle automatisch sortiert (aufsteigend und absteigend), wenn der Benutzer auf die Header-Zelle klickt.
Filterung#
Fügen wir ein Sucheingabefeld hinzu, um E-Mails in unserer Tabelle zu filtern.
<DataTable> aktualisieren#
"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 (
<div>
<div className="flex items-center py-4">
<Input
placeholder="Filter emails..."
value={(table.getColumn("email")?.getFilterValue() as string) ?? ""}
onChange={(event) =>
table.getColumn("email")?.setFilterValue(event.target.value)
}
className="max-w-sm"
/>
</div>
<div className="overflow-hidden rounded-md border">
<Table>{ ... }</Table>
</div>
</div>
)
}Die Filterung ist jetzt für die email-Spalte aktiviert. Du kannst auch Filter zu anderen Spalten hinzufügen. Weitere Informationen zum Anpassen von Filtern findest du in der Filter-Dokumentation.
Sichtbarkeit#
Das Hinzufügen der Spaltensichtbarkeit ist mit der Sichtbarkeits-API von @tanstack/react-table recht einfach.
<DataTable> aktualisieren#
"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 (
<div>
<div className="flex items-center py-4">
<Input
placeholder="Filter emails..."
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="ml-auto">
Columns
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
{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>
)
})}
</DropdownMenuContent>
</DropdownMenu>
</div>
<div className="overflow-hidden rounded-md border">
<Table>{ ... }</Table>
</div>
</div>
)
}Dadurch wird ein Dropdown-Menü hinzugefügt, mit dem du die Spaltensichtbarkeit umschalten kannst.
Zeilenauswahl#
Als Nächstes fügen wir unserer Tabelle die Zeilenauswahl hinzu.
Spaltendefinitionen aktualisieren#
"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,
},
]<DataTable> aktualisieren#
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 (
<div>
<div className="overflow-hidden rounded-md border">
<Table />
</div>
</div>
)
}Dadurch wird jeder Zeile eine Checkbox und dem Header eine Checkbox zum Auswählen aller Zeilen hinzugefügt.
Ausgewählte Zeilen anzeigen#
Du kannst die Anzahl der ausgewählten Zeilen mithilfe der table.getFilteredSelectedRowModel()-API anzeigen.
<div className="flex-1 text-sm text-muted-foreground">
{table.getFilteredSelectedRowModel().rows.length} of{" "}
{table.getFilteredRowModel().rows.length} row(s) selected.
</div>Wiederverwendbare Komponenten#
Hier sind einige Komponenten, die du zum Erstellen deiner Datentabellen verwenden kannst. Sie stammen aus der Tasks-Demo.
Spalten-Header#
Mache jeden Spalten-Header sortierbar und ausblendbar.
import { type Column } from "@tanstack/react-table"
import { ArrowDown, ArrowUp, ChevronsUpDown, EyeOff } from "lucide-react"
import { cn } from "@/lib/utils"
import { Button } from "@/registry/base/ui/button"
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuSeparator,
DropdownMenuTrigger,
} from "@/registry/base/ui/dropdown-menu"
interface DataTableColumnHeaderProps<
TData,
TValue,
> extends React.HTMLAttributes<HTMLDivElement> {
column: Column<TData, TValue>
title: string
}
export function DataTableColumnHeader<TData, TValue>({
column,
title,
className,
}: DataTableColumnHeaderProps<TData, TValue>) {
if (!column.getCanSort()) {
return <div className={cn(className)}>{title}</div>
}
return (
<div className={cn("flex items-center gap-2", className)}>
<DropdownMenu>
<DropdownMenuTrigger
render={
<Button
variant="ghost"
size="sm"
className="-ml-3 h-8 data-popup-open:bg-accent"
/>
}
>
<span>{title}</span>
{column.getIsSorted() === "desc" ? (
<ArrowDown />
) : column.getIsSorted() === "asc" ? (
<ArrowUp />
) : (
<ChevronsUpDown />
)}
</DropdownMenuTrigger>
<DropdownMenuContent align="start">
<DropdownMenuItem onClick={() => column.toggleSorting(false)}>
<ArrowUp />
Asc
</DropdownMenuItem>
<DropdownMenuItem onClick={() => column.toggleSorting(true)}>
<ArrowDown />
Desc
</DropdownMenuItem>
<DropdownMenuSeparator />
<DropdownMenuItem onClick={() => column.toggleVisibility(false)}>
<EyeOff />
Hide
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</div>
)
}
export const columns = [
{
accessorKey: "email",
header: ({ column }) => (
<DataTableColumnHeader column={column} title="Email" />
),
},
]Paginierung#
Füge deiner Tabelle Paginierungssteuerelemente hinzu, einschließlich Seitengröße und Auswahlanzahl.
import { type Table } from "@tanstack/react-table"
import {
ChevronLeft,
ChevronRight,
ChevronsLeft,
ChevronsRight,
} from "lucide-react"
import { Button } from "@/registry/base/ui/button"
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/registry/base/ui/select"
const pageSizeItems = [10, 20, 25, 30, 40, 50].map((pageSize) => ({
label: `${pageSize}`,
value: `${pageSize}`,
}))
interface DataTablePaginationProps<TData> {
table: Table<TData>
}
export function DataTablePagination<TData>({
table,
}: DataTablePaginationProps<TData>) {
return (
<div className="flex items-center justify-between px-2">
<div className="flex-1 text-sm text-muted-foreground">
{table.getFilteredSelectedRowModel().rows.length} of{" "}
{table.getFilteredRowModel().rows.length} row(s) selected.
</div>
<div className="flex items-center space-x-6 lg:space-x-8">
<div className="flex items-center space-x-2">
<p className="text-sm font-medium">Rows per page</p>
<Select
items={pageSizeItems}
value={`${table.getState().pagination.pageSize}`}
onValueChange={(value) => {
if (value !== null) {
table.setPageSize(Number(value))
}
}}
>
<SelectTrigger className="h-8 w-[70px]">
<SelectValue />
</SelectTrigger>
<SelectContent side="top">
{pageSizeItems.map((item) => (
<SelectItem key={item.value} value={item.value}>
{item.label}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
<div className="flex w-[100px] items-center justify-center text-sm font-medium">
Page {table.getState().pagination.pageIndex + 1} of{" "}
{table.getPageCount()}
</div>
<div className="flex items-center space-x-2">
<Button
variant="outline"
size="icon"
className="hidden size-8 lg:flex"
onClick={() => table.setPageIndex(0)}
disabled={!table.getCanPreviousPage()}
>
<span className="sr-only">Go to first page</span>
<ChevronsLeft />
</Button>
<Button
variant="outline"
size="icon"
className="size-8"
onClick={() => table.previousPage()}
disabled={!table.getCanPreviousPage()}
>
<span className="sr-only">Go to previous page</span>
<ChevronLeft />
</Button>
<Button
variant="outline"
size="icon"
className="size-8"
onClick={() => table.nextPage()}
disabled={!table.getCanNextPage()}
>
<span className="sr-only">Go to next page</span>
<ChevronRight />
</Button>
<Button
variant="outline"
size="icon"
className="hidden size-8 lg:flex"
onClick={() => table.setPageIndex(table.getPageCount() - 1)}
disabled={!table.getCanNextPage()}
>
<span className="sr-only">Go to last page</span>
<ChevronsRight />
</Button>
</div>
</div>
</div>
)
}
<DataTablePagination table={table} />Spalten umschalten#
Eine Komponente zum Umschalten der Spaltensichtbarkeit.
"use client"
import { type Table } from "@tanstack/react-table"
import { Settings2 } from "lucide-react"
import { Button } from "@/registry/base/ui/button"
import {
DropdownMenu,
DropdownMenuCheckboxItem,
DropdownMenuContent,
DropdownMenuGroup,
DropdownMenuLabel,
DropdownMenuSeparator,
DropdownMenuTrigger,
} from "@/registry/base/ui/dropdown-menu"
export function DataTableViewOptions<TData>({
table,
}: {
table: Table<TData>
}) {
return (
<DropdownMenu>
<DropdownMenuTrigger
render={
<Button
variant="outline"
size="sm"
className="ml-auto hidden h-8 lg:flex"
/>
}
>
<Settings2 />
View
</DropdownMenuTrigger>
<DropdownMenuContent align="end" className="w-[150px]">
{/* GroupLabel (Base UI) needs a Group ancestor or it throws error #31. */}
<DropdownMenuGroup>
<DropdownMenuLabel>Toggle columns</DropdownMenuLabel>
</DropdownMenuGroup>
<DropdownMenuSeparator />
{table
.getAllColumns()
.filter(
(column) =>
typeof column.accessorFn !== "undefined" && column.getCanHide()
)
.map((column) => {
return (
<DropdownMenuCheckboxItem
key={column.id}
className="capitalize"
checked={column.getIsVisible()}
onCheckedChange={(value) => column.toggleVisibility(!!value)}
>
{column.id}
</DropdownMenuCheckboxItem>
)
})}
</DropdownMenuContent>
</DropdownMenu>
)
}
<DataTableViewOptions table={table} />RTL#
Um RTL-Unterstützung in Neobrutalism zu aktivieren, siehe die RTL-Konfigurationsanleitung.
Barrierefreiheit#
TanStack Table ist headless – es rendert kein Markup, die Barrierefreiheit kommt also von den Elementen in dieser Anleitung. Die <Table />-Komponente gibt native <table>-, <th>- und <td>-Elemente aus, die Screenreadern Zeilen- und Spaltenkontext gemäß dem WAI-ARIA-Table-Pattern geben. Zwei Dinge solltest du beim Bauen beibehalten: Setze aria-sort auf den aktuell sortierten Spaltenkopf und behalte das aria-label auf den Auswahl-Checkboxen sowie den sr-only-Text auf reinen Icon-Triggern (die Snippets oben tun beides).
Jedes Steuerelement in dieser Anleitung ist ein nativer Button, eine native Checkbox oder ein natives Eingabefeld, das Tastaturverhalten entspricht also dem Browser-Standard:
| Taste | Aktion |
|---|---|
Tab / Shift + Tab | Bewegt den Fokus durch die Steuerelemente der Tabelle – Filterfeld, Sortier-Spaltenköpfe, Zeilenmenüs, Paginierung |
Space / Enter | Aktiviert den fokussierten Sortier- oder Paginierungsbutton |
Space | Schaltet die fokussierte Zeilen- oder „Alle auswählen“-Checkbox um |