HextaUI

Data table

Una tabla para datos reales, con ordenación, búsqueda, selección de filas, columnas fijadas, un encabezado fijo y paginación.

MethodCountry
[email protected]paidCardUnited States2026-01-01$5.00
[email protected]pendingPayPalJapan2026-02-02$84.20
[email protected]failedBankGermany2026-03-03$163.40
[email protected]paidApple PayBrazil2026-04-04$242.60
[email protected]pendingCardIndia2026-05-05$321.80
[email protected]failedPayPalUnited States2026-06-06$401.00
[email protected]paidBankJapan2026-07-07$480.10
[email protected]pendingApple PayGermany2026-08-08$559.30
[email protected]failedCardBrazil2026-09-09$638.50
[email protected]paidPayPalIndia2026-01-10$717.70
0 of 42 rows selected
Page 1 of 5
"use client"

import { Badge, BadgeDot } from "@/components/ui/badge"
import {
  createDataTableColumns,
  DataTable,
  DataTableColumnHeader,
  DataTableContent,
  DataTablePagination,
  DataTableSearch,
  DataTableSelectAll,
  DataTableSelectRow,
  DataTableToolbar,
  DataTableViewOptions,
  useDataTable,
} from "@/components/ui/data-table"

type Payment = {
  id: string
  email: string
  status: "paid" | "pending" | "failed"
  amount: number
  method: string
  country: string
  date: string
}

const statuses = ["paid", "pending", "failed"] as const
const methods = ["Card", "PayPal", "Bank", "Apple Pay"]
const countries = ["United States", "Germany", "India", "Japan", "Brazil"]
const names = ["olivia", "jackson", "isabella", "william", "sofia", "liam"]

function makePayments(count: number): Payment[] {
  return Array.from({ length: count }, (_, index) => ({
    id: `pay_${String(index + 1).padStart(4, "0")}`,
    email: `${names[index % names.length]}.${Math.floor(index / names.length) + 1}@example.com`,
    status: statuses[(index * 7) % 3],
    amount: Math.round(((index * 7919) % 100000) / 10) / 10 + 5,
    method: methods[index % methods.length],
    country: countries[(index * 3) % countries.length],
    date: `2026-${String((index % 9) + 1).padStart(2, "0")}-${String((index % 27) + 1).padStart(2, "0")}`,
  }))
}

const payments = makePayments(42)

const currency = new Intl.NumberFormat("en-US", {
  style: "currency",
  currency: "USD",
})

const statusVariant = {
  paid: "success",
  pending: "warning",
  failed: "destructive",
} as const

const helper = createDataTableColumns<Payment>()

const columns = helper.columns([
  helper.display({
    id: "select",
    header: ({ table }) => <DataTableSelectAll table={table} />,
    cell: ({ row }) => <DataTableSelectRow row={row} />,
    enableSorting: false,
    enableHiding: false,
  }),
  helper.accessor("email", {
    header: ({ column }) => (
      <DataTableColumnHeader column={column} title="Email" />
    ),
  }),
  helper.accessor("status", {
    header: ({ column }) => (
      <DataTableColumnHeader column={column} title="Status" />
    ),
    cell: ({ getValue }) => {
      const status = getValue()
      return (
        <Badge variant={statusVariant[status]}>
          <BadgeDot />
          <span className="capitalize">{status}</span>
        </Badge>
      )
    },
  }),
  helper.accessor("method", { header: "Method" }),
  helper.accessor("country", { header: "Country" }),
  helper.accessor("date", {
    header: ({ column }) => (
      <DataTableColumnHeader column={column} title="Date" />
    ),
  }),
  helper.accessor("amount", {
    header: ({ column }) => (
      <DataTableColumnHeader column={column} title="Amount" />
    ),
    cell: ({ getValue }) => currency.format(getValue()),
    meta: { align: "end" },
  }),
])

export function DataTableDemo() {
  const table = useDataTable({
    data: payments,
    columns,
    getRowId: (row) => row.id,
  })

  return (
    <DataTable table={table}>
      <DataTableToolbar>
        <DataTableSearch placeholder="Search payments…" />
        <DataTableViewOptions />
      </DataTableToolbar>
      <DataTableContent emptyMessage="No payments match your search." />
      <DataTablePagination />
    </DataTable>
  )
}
pnpm dlx shadcn@latest add https://hextaui.com/r/data-table.json

Añade el componente, los tokens del tema de HextaUI y los componentes de HextaUI de los que depende.

La data table es TanStack Table v9 con las funciones de ordenación, filtrado, paginación, selección y visibilidad de columnas ya conectadas. Define las columnas una vez con createDataTableColumns, crea la tabla con useDataTable y luego compón las partes que necesites.

import {
  createDataTableColumns,
  DataTable,
  DataTableColumnHeader,
  DataTableContent,
  DataTablePagination,
  DataTableSearch,
  DataTableSelectAll,
  DataTableSelectRow,
  DataTableToolbar,
  DataTableViewOptions,
  useDataTable,
} from "@/components/ui/data-table"
type Payment = { id: string; email: string; amount: number }

const helper = createDataTableColumns<Payment>()

const columns = helper.columns([
  helper.display({
    id: "select",
    header: ({ table }) => <DataTableSelectAll table={table} />,
    cell: ({ row }) => <DataTableSelectRow row={row} />,
    enableSorting: false,
    enableHiding: false,
  }),
  helper.accessor("email", {
    header: ({ column }) => (
      <DataTableColumnHeader column={column} title="Email" />
    ),
  }),
  helper.accessor("amount", {
    header: "Amount",
    meta: { align: "end" },
  }),
])
export function Payments({ data }: { data: Payment[] }) {
  const table = useDataTable({
    data,
    columns,
    getRowId: (row) => row.id,
  })

  return (
    <DataTable table={table}>
      <DataTableToolbar>
        <DataTableSearch placeholder="Search payments…" />
        <DataTableViewOptions />
      </DataTableToolbar>
      <DataTableContent />
      <DataTablePagination />
    </DataTable>
  )
}

Pasa cualquier estado de TanStack que quieras controlar, como sorting o la selección de filas, con su manejador de cambio.

const [sorting, setSorting] = React.useState([{ id: "date", desc: true }])

const table = useDataTable({
  data,
  columns,
  state: { sorting },
  onSortingChange: setSorting,
})
DataTable
├── DataTableToolbar
│   ├── DataTableSearch
│   └── DataTableViewOptions
├── DataTableContent
└── DataTablePagination

DataTableColumnHeader, DataTableSelectAll y DataTableSelectRow van en las definiciones de tus columnas, como encabezado o celda de una columna.

Tabla simple

Las partes simples de <Table /> con las que se construye la data table. Úsalas por separado para datos estáticos, con un caption y un total en el pie.

import {
  Table,
  TableBody,
  TableCaption,
  TableCell,
  TableFooter,
  TableHead,
  TableHeader,
  TableRow,
} from "@/components/ui/table"

const invoices = [
  { id: "INV001", status: "Paid", method: "Credit card", amount: 250 },
  { id: "INV002", status: "Pending", method: "PayPal", amount: 150 },
  { id: "INV003", status: "Unpaid", method: "Bank transfer", amount: 350 },
  { id: "INV004", status: "Paid", method: "Credit card", amount: 450 },
]

const currency = new Intl.NumberFormat("en-US", {
  style: "currency",
  currency: "USD",
})

export function TableDemo() {
  const total = invoices.reduce((sum, invoice) => sum + invoice.amount, 0)

  return (
    <Table>
      <TableCaption>A list of your recent invoices.</TableCaption>
      <TableHeader>
        <TableRow>
          <TableHead>Invoice</TableHead>
          <TableHead>Status</TableHead>
          <TableHead>Method</TableHead>
          <TableHead align="end">Amount</TableHead>
        </TableRow>
      </TableHeader>
      <TableBody>
        {invoices.map((invoice) => (
          <TableRow key={invoice.id}>
            <TableCell>{invoice.id}</TableCell>
            <TableCell>{invoice.status}</TableCell>
            <TableCell>{invoice.method}</TableCell>
            <TableCell align="end">{currency.format(invoice.amount)}</TableCell>
          </TableRow>
        ))}
      </TableBody>
      <TableFooter>
        <TableRow>
          <TableCell colSpan={3}>Total</TableCell>
          <TableCell align="end">{currency.format(total)}</TableCell>
        </TableRow>
      </TableFooter>
    </Table>
  )
}

Cargando

Con loading, las filas skeleton ocupan el mismo espacio que las filas reales, así que nada salta cuando llegan los datos. Mientras tanto, la tabla se marca con aria-busy.

"use client"

import * as React from "react"

import { Badge, BadgeDot } from "@/components/ui/badge"
import { Button } from "@/components/ui/button"
import {
  createDataTableColumns,
  DataTable,
  DataTableColumnHeader,
  DataTableContent,
  DataTablePagination,
  DataTableSearch,
  DataTableSelectAll,
  DataTableSelectRow,
  DataTableToolbar,
  DataTableViewOptions,
  useDataTable,
} from "@/components/ui/data-table"

type Payment = {
  id: string
  email: string
  status: "paid" | "pending" | "failed"
  amount: number
  method: string
  country: string
  date: string
}

const statuses = ["paid", "pending", "failed"] as const
const methods = ["Card", "PayPal", "Bank", "Apple Pay"]
const countries = ["United States", "Germany", "India", "Japan", "Brazil"]
const names = ["olivia", "jackson", "isabella", "william", "sofia", "liam"]

function makePayments(count: number): Payment[] {
  return Array.from({ length: count }, (_, index) => ({
    id: `pay_${String(index + 1).padStart(4, "0")}`,
    email: `${names[index % names.length]}.${Math.floor(index / names.length) + 1}@example.com`,
    status: statuses[(index * 7) % 3],
    amount: Math.round(((index * 7919) % 100000) / 10) / 10 + 5,
    method: methods[index % methods.length],
    country: countries[(index * 3) % countries.length],
    date: `2026-${String((index % 9) + 1).padStart(2, "0")}-${String((index % 27) + 1).padStart(2, "0")}`,
  }))
}

const payments = makePayments(5)

const currency = new Intl.NumberFormat("en-US", {
  style: "currency",
  currency: "USD",
})

const statusVariant = {
  paid: "success",
  pending: "warning",
  failed: "destructive",
} as const

const helper = createDataTableColumns<Payment>()

const columns = helper.columns([
  helper.display({
    id: "select",
    header: ({ table }) => <DataTableSelectAll table={table} />,
    cell: ({ row }) => <DataTableSelectRow row={row} />,
    enableSorting: false,
    enableHiding: false,
  }),
  helper.accessor("email", {
    header: ({ column }) => (
      <DataTableColumnHeader column={column} title="Email" />
    ),
  }),
  helper.accessor("status", {
    header: ({ column }) => (
      <DataTableColumnHeader column={column} title="Status" />
    ),
    cell: ({ getValue }) => {
      const status = getValue()
      return (
        <Badge variant={statusVariant[status]}>
          <BadgeDot />
          <span className="capitalize">{status}</span>
        </Badge>
      )
    },
  }),
  helper.accessor("method", { header: "Method" }),
  helper.accessor("country", { header: "Country" }),
  helper.accessor("date", {
    header: ({ column }) => (
      <DataTableColumnHeader column={column} title="Date" />
    ),
  }),
  helper.accessor("amount", {
    header: ({ column }) => (
      <DataTableColumnHeader column={column} title="Amount" />
    ),
    cell: ({ getValue }) => currency.format(getValue()),
    meta: { align: "end" },
  }),
])

export function DataTableLoading() {
  const [loading, setLoading] = React.useState(true)
  const table = useDataTable({
    data: payments,
    columns,
    getRowId: (row) => row.id,
  })

  return (
    <div className="flex w-full flex-col items-start gap-3">
      <Button
        variant="outline"
        size="sm"
        onClick={() => setLoading((value) => !value)}
      >
        {loading ? "Finish loading" : "Load again"}
      </Button>
      <DataTable table={table}>
        <DataTableToolbar>
          <DataTableSearch placeholder="Search payments…" />
          <DataTableViewOptions />
        </DataTableToolbar>
        <DataTableContent loading={loading} />
        <DataTablePagination />
      </DataTable>
    </div>
  )
}

Empty

emptyMessage llena el cuerpo cuando no hay datos o nada coincide con la búsqueda.

"use client"

import { Badge, BadgeDot } from "@/components/ui/badge"
import {
  createDataTableColumns,
  DataTable,
  DataTableColumnHeader,
  DataTableContent,
  DataTablePagination,
  DataTableSearch,
  DataTableSelectAll,
  DataTableSelectRow,
  DataTableToolbar,
  DataTableViewOptions,
  useDataTable,
} from "@/components/ui/data-table"

type Payment = {
  id: string
  email: string
  status: "paid" | "pending" | "failed"
  amount: number
  method: string
  country: string
  date: string
}

const statuses = ["paid", "pending", "failed"] as const
const methods = ["Card", "PayPal", "Bank", "Apple Pay"]
const countries = ["United States", "Germany", "India", "Japan", "Brazil"]
const names = ["olivia", "jackson", "isabella", "william", "sofia", "liam"]

function makePayments(count: number): Payment[] {
  return Array.from({ length: count }, (_, index) => ({
    id: `pay_${String(index + 1).padStart(4, "0")}`,
    email: `${names[index % names.length]}.${Math.floor(index / names.length) + 1}@example.com`,
    status: statuses[(index * 7) % 3],
    amount: Math.round(((index * 7919) % 100000) / 10) / 10 + 5,
    method: methods[index % methods.length],
    country: countries[(index * 3) % countries.length],
    date: `2026-${String((index % 9) + 1).padStart(2, "0")}-${String((index % 27) + 1).padStart(2, "0")}`,
  }))
}

const payments: Payment[] = []

const currency = new Intl.NumberFormat("en-US", {
  style: "currency",
  currency: "USD",
})

const statusVariant = {
  paid: "success",
  pending: "warning",
  failed: "destructive",
} as const

const helper = createDataTableColumns<Payment>()

const columns = helper.columns([
  helper.display({
    id: "select",
    header: ({ table }) => <DataTableSelectAll table={table} />,
    cell: ({ row }) => <DataTableSelectRow row={row} />,
    enableSorting: false,
    enableHiding: false,
  }),
  helper.accessor("email", {
    header: ({ column }) => (
      <DataTableColumnHeader column={column} title="Email" />
    ),
  }),
  helper.accessor("status", {
    header: ({ column }) => (
      <DataTableColumnHeader column={column} title="Status" />
    ),
    cell: ({ getValue }) => {
      const status = getValue()
      return (
        <Badge variant={statusVariant[status]}>
          <BadgeDot />
          <span className="capitalize">{status}</span>
        </Badge>
      )
    },
  }),
  helper.accessor("method", { header: "Method" }),
  helper.accessor("country", { header: "Country" }),
  helper.accessor("date", {
    header: ({ column }) => (
      <DataTableColumnHeader column={column} title="Date" />
    ),
  }),
  helper.accessor("amount", {
    header: ({ column }) => (
      <DataTableColumnHeader column={column} title="Amount" />
    ),
    cell: ({ getValue }) => currency.format(getValue()),
    meta: { align: "end" },
  }),
])

export function DataTableEmpty() {
  const table = useDataTable({
    data: payments,
    columns,
    getRowId: (row) => row.id,
  })

  return (
    <DataTable table={table}>
      <DataTableToolbar>
        <DataTableSearch placeholder="Search payments…" />
        <DataTableViewOptions />
      </DataTableToolbar>
      <DataTableContent emptyMessage="No payments yet." />
      <DataTablePagination />
    </DataTable>
  )
}

Dale al contenedor una altura máxima con containerClassName y define stickyHeader. El encabezado permanece fijo y gana una línea fina cuando las filas se desplazan bajo él. Shift-arrastrar sobre los checkboxes desplaza la caja al acercarte a su borde.

"use client"

import { Badge, BadgeDot } from "@/components/ui/badge"
import {
  createDataTableColumns,
  DataTable,
  DataTableColumnHeader,
  DataTableContent,
  DataTablePagination,
  DataTableSearch,
  DataTableSelectAll,
  DataTableSelectRow,
  DataTableToolbar,
  DataTableViewOptions,
  useDataTable,
} from "@/components/ui/data-table"

type Payment = {
  id: string
  email: string
  status: "paid" | "pending" | "failed"
  amount: number
  method: string
  country: string
  date: string
}

const statuses = ["paid", "pending", "failed"] as const
const methods = ["Card", "PayPal", "Bank", "Apple Pay"]
const countries = ["United States", "Germany", "India", "Japan", "Brazil"]
const names = ["olivia", "jackson", "isabella", "william", "sofia", "liam"]

function makePayments(count: number): Payment[] {
  return Array.from({ length: count }, (_, index) => ({
    id: `pay_${String(index + 1).padStart(4, "0")}`,
    email: `${names[index % names.length]}.${Math.floor(index / names.length) + 1}@example.com`,
    status: statuses[(index * 7) % 3],
    amount: Math.round(((index * 7919) % 100000) / 10) / 10 + 5,
    method: methods[index % methods.length],
    country: countries[(index * 3) % countries.length],
    date: `2026-${String((index % 9) + 1).padStart(2, "0")}-${String((index % 27) + 1).padStart(2, "0")}`,
  }))
}

const payments = makePayments(1000)

const currency = new Intl.NumberFormat("en-US", {
  style: "currency",
  currency: "USD",
})

const statusVariant = {
  paid: "success",
  pending: "warning",
  failed: "destructive",
} as const

const helper = createDataTableColumns<Payment>()

const columns = helper.columns([
  helper.display({
    id: "select",
    header: ({ table }) => <DataTableSelectAll table={table} />,
    cell: ({ row }) => <DataTableSelectRow row={row} />,
    enableSorting: false,
    enableHiding: false,
  }),
  helper.accessor("email", {
    header: ({ column }) => (
      <DataTableColumnHeader column={column} title="Email" />
    ),
  }),
  helper.accessor("status", {
    header: ({ column }) => (
      <DataTableColumnHeader column={column} title="Status" />
    ),
    cell: ({ getValue }) => {
      const status = getValue()
      return (
        <Badge variant={statusVariant[status]}>
          <BadgeDot />
          <span className="capitalize">{status}</span>
        </Badge>
      )
    },
  }),
  helper.accessor("method", { header: "Method" }),
  helper.accessor("country", { header: "Country" }),
  helper.accessor("date", {
    header: ({ column }) => (
      <DataTableColumnHeader column={column} title="Date" />
    ),
  }),
  helper.accessor("amount", {
    header: ({ column }) => (
      <DataTableColumnHeader column={column} title="Amount" />
    ),
    cell: ({ getValue }) => currency.format(getValue()),
    meta: { align: "end" },
  }),
])

export function DataTableStickyHeader() {
  const table = useDataTable({
    data: payments,
    columns,
    getRowId: (row) => row.id,
    initialState: { pagination: { pageIndex: 0, pageSize: 100 } },
  })

  return (
    <DataTable table={table}>
      <DataTableToolbar>
        <DataTableSearch placeholder="Search 1,000 payments…" />
        <DataTableViewOptions />
      </DataTableToolbar>
      <DataTableContent stickyHeader containerClassName="max-h-80" />
      <DataTablePagination />
    </DataTable>
  )
}

Columnas ancladas

La columna de selección y la primera columna de datos están ancladas por defecto. Elige las tuyas con pinStart. Una sombra suave marca el borde cuando la tabla se desplaza hacia los lados.

"use client"

import { Badge, BadgeDot } from "@/components/ui/badge"
import {
  createDataTableColumns,
  DataTable,
  DataTableColumnHeader,
  DataTableContent,
  DataTablePagination,
  DataTableSearch,
  DataTableSelectAll,
  DataTableSelectRow,
  DataTableToolbar,
  DataTableViewOptions,
  useDataTable,
} from "@/components/ui/data-table"

type Payment = {
  id: string
  email: string
  status: "paid" | "pending" | "failed"
  amount: number
  method: string
  country: string
  date: string
}

const statuses = ["paid", "pending", "failed"] as const
const methods = ["Card", "PayPal", "Bank", "Apple Pay"]
const countries = ["United States", "Germany", "India", "Japan", "Brazil"]
const names = ["olivia", "jackson", "isabella", "william", "sofia", "liam"]

function makePayments(count: number): Payment[] {
  return Array.from({ length: count }, (_, index) => ({
    id: `pay_${String(index + 1).padStart(4, "0")}`,
    email: `${names[index % names.length]}.${Math.floor(index / names.length) + 1}@example.com`,
    status: statuses[(index * 7) % 3],
    amount: Math.round(((index * 7919) % 100000) / 10) / 10 + 5,
    method: methods[index % methods.length],
    country: countries[(index * 3) % countries.length],
    date: `2026-${String((index % 9) + 1).padStart(2, "0")}-${String((index % 27) + 1).padStart(2, "0")}`,
  }))
}

const payments = makePayments(12)

const currency = new Intl.NumberFormat("en-US", {
  style: "currency",
  currency: "USD",
})

const statusVariant = {
  paid: "success",
  pending: "warning",
  failed: "destructive",
} as const

const helper = createDataTableColumns<Payment>()

const columns = helper.columns([
  helper.display({
    id: "select",
    header: ({ table }) => <DataTableSelectAll table={table} />,
    cell: ({ row }) => <DataTableSelectRow row={row} />,
    enableSorting: false,
    enableHiding: false,
  }),
  helper.accessor("email", {
    header: ({ column }) => (
      <DataTableColumnHeader column={column} title="Email" />
    ),
  }),
  helper.accessor("status", {
    header: ({ column }) => (
      <DataTableColumnHeader column={column} title="Status" />
    ),
    cell: ({ getValue }) => {
      const status = getValue()
      return (
        <Badge variant={statusVariant[status]}>
          <BadgeDot />
          <span className="capitalize">{status}</span>
        </Badge>
      )
    },
  }),
  helper.accessor("method", { header: "Method" }),
  helper.accessor("country", { header: "Country" }),
  helper.accessor("date", {
    header: ({ column }) => (
      <DataTableColumnHeader column={column} title="Date" />
    ),
  }),
  helper.accessor("amount", {
    header: ({ column }) => (
      <DataTableColumnHeader column={column} title="Amount" />
    ),
    cell: ({ getValue }) => currency.format(getValue()),
    meta: { align: "end" },
  }),
])

export function DataTablePinnedColumns() {
  const table = useDataTable({
    data: payments,
    columns,
    getRowId: (row) => row.id,
  })

  return (
    <div className="w-full max-w-md">
      <DataTable table={table}>
        <DataTableToolbar>
          <DataTableSearch placeholder="Search payments…" />
          <DataTableViewOptions />
        </DataTableToolbar>
        <DataTableContent pinStart={["select", "email", "status"]} />
        <DataTablePagination />
      </DataTable>
    </div>
  )
}

De derecha a izquierda

Todas las etiquetas y contadores se pueden reemplazar con labels y las funciones de formato. Las flechas de paginación y las columnas ancladas se invierten con la dirección.

"use client"

import { Badge, BadgeDot } from "@/components/ui/badge"
import {
  createDataTableColumns,
  DataTable,
  DataTableColumnHeader,
  DataTableContent,
  DataTablePagination,
  DataTableSearch,
  DataTableSelectAll,
  DataTableSelectRow,
  DataTableToolbar,
  DataTableViewOptions,
  useDataTable,
} from "@/components/ui/data-table"

type Payment = {
  id: string
  email: string
  status: "paid" | "pending" | "failed"
  amount: number
  method: string
  country: string
  date: string
}

const statuses = ["paid", "pending", "failed"] as const
const methods = ["Card", "PayPal", "Bank", "Apple Pay"]
const countries = ["United States", "Germany", "India", "Japan", "Brazil"]
const names = ["olivia", "jackson", "isabella", "william", "sofia", "liam"]

function makePayments(count: number): Payment[] {
  return Array.from({ length: count }, (_, index) => ({
    id: `pay_${String(index + 1).padStart(4, "0")}`,
    email: `${names[index % names.length]}.${Math.floor(index / names.length) + 1}@example.com`,
    status: statuses[(index * 7) % 3],
    amount: Math.round(((index * 7919) % 100000) / 10) / 10 + 5,
    method: methods[index % methods.length],
    country: countries[(index * 3) % countries.length],
    date: `2026-${String((index % 9) + 1).padStart(2, "0")}-${String((index % 27) + 1).padStart(2, "0")}`,
  }))
}

const payments = makePayments(6)

const currency = new Intl.NumberFormat("en-US", {
  style: "currency",
  currency: "USD",
})

const statusVariant = {
  paid: "success",
  pending: "warning",
  failed: "destructive",
} as const

const helper = createDataTableColumns<Payment>()

const columns = helper.columns([
  helper.display({
    id: "select",
    header: ({ table }) => <DataTableSelectAll table={table} />,
    cell: ({ row }) => <DataTableSelectRow row={row} />,
    enableSorting: false,
    enableHiding: false,
  }),
  helper.accessor("email", {
    header: ({ column }) => (
      <DataTableColumnHeader column={column} title="Email" />
    ),
  }),
  helper.accessor("status", {
    header: ({ column }) => (
      <DataTableColumnHeader column={column} title="Status" />
    ),
    cell: ({ getValue }) => {
      const status = getValue()
      return (
        <Badge variant={statusVariant[status]}>
          <BadgeDot />
          <span className="capitalize">{status}</span>
        </Badge>
      )
    },
  }),
  helper.accessor("method", { header: "Method" }),
  helper.accessor("country", { header: "Country" }),
  helper.accessor("date", {
    header: ({ column }) => (
      <DataTableColumnHeader column={column} title="Date" />
    ),
  }),
  helper.accessor("amount", {
    header: ({ column }) => (
      <DataTableColumnHeader column={column} title="Amount" />
    ),
    cell: ({ getValue }) => currency.format(getValue()),
    meta: { align: "end" },
  }),
])

const arabicNumber = new Intl.NumberFormat("ar")

export function DataTableRtl() {
  const table = useDataTable({
    data: payments,
    columns,
    getRowId: (row) => row.id,
  })

  return (
    <div dir="rtl" className="w-full">
      <DataTable table={table}>
        <DataTableToolbar>
          <DataTableSearch placeholder="ابحث في المدفوعات…" />
          <DataTableViewOptions label="عرض" groupLabel="إظهار الأعمدة" />
        </DataTableToolbar>
        <DataTableContent emptyMessage="لا توجد نتائج." />
        <DataTablePagination
          labels={{
            rowsPerPage: "صفوف في الصفحة",
            firstPage: "الصفحة الأولى",
            previousPage: "الصفحة السابقة",
            nextPage: "الصفحة التالية",
            lastPage: "الصفحة الأخيرة",
          }}
          formatSelection={(selected, total) =>
            `تم تحديد ${arabicNumber.format(selected)} من ${arabicNumber.format(total)}`
          }
          formatPage={(page, count) =>
            `صفحة ${arabicNumber.format(page)} من ${arabicNumber.format(count)}`
          }
        />
      </DataTable>
    </div>
  )
}
  • Haz clic en un encabezado ordenable para ordenar de forma ascendente, de nuevo para descendente, y una tercera vez para quitar el orden. Shift-clic en otro encabezado para añadir un orden secundario.
  • Shift-clic en el checkbox de una fila para seleccionar todas las filas entre ella y la última en que hiciste clic. Mantén Shift y arrastra sobre los checkboxes para seleccionar o borrar un rango de una sola vez.
  • La búsqueda coincide con todas las columnas excepto la columna de selección y vuelve a la primera página.
  • Ocultar columnas desde el menú View mantiene el menú abierto, así que puedes alternar varias a la vez.
KeyAcción
TabRecorre la búsqueda, el menú View, los encabezados ordenables, los checkboxes de fila y la paginación.
EnterSpaceOrdena por el encabezado con foco.
SpaceAlterna el checkbox con foco.
EscBorra la búsqueda cuando tiene texto.
  • Los encabezados ordenables llevan aria-sort, y una región activa polite anuncia el nuevo orden y, poco después de escribir, el número de resultados.
  • El indicador de página es una región activa, así que los lectores de pantalla oyen la nueva página tras pulsar siguiente o anterior.
  • Los checkboxes tienen etiquetas por defecto. Reemplázalas con aria-label en <DataTableSelectAll /> y <DataTableSelectRow />.

Cada parte de abajo debe renderizarse dentro de <DataTable />, que comparte la tabla con ellas.

Acepta las opciones de TanStack Table y devuelve la tabla. Las páginas contienen 10 filas salvo que initialState.pagination indique otra cosa.

PropTipoPredeterminado
data
TData[]–
columnsConstrúyelas con createDataTableColumns.
ColumnDef[]–
getRowIdMantiene estable la selección cuando las filas se mueven. Por defecto es el índice de la fila.
(row: TData) => string–
initialState
Partial<TableState>{ pagination: { pageIndex: 0, pageSize: 10 } }
stateControla sorting, rowSelection, globalFilter, pagination o columnVisibility.
Partial<TableState>–
onSortingChangeCada estado controlable tiene un manejador correspondiente, como onRowSelectionChange.
OnChangeFn<SortingState>–
enableRowSelection
boolean | (row) => booleantrue

Devuelve un column helper tipado con accessor, display y columns. Define meta: { align: "end" } en las columnas numéricas para alinear el encabezado y las celdas.

PropTipoPredeterminado
tableLa tabla devuelta por useDataTable.
DataTableInstance<TData>–
className
string–
AtributoDescripción
data-slot="data-table"El contenedor alrededor de todas las partes.
data-slot="data-table-announcer"La región activa oculta visualmente.
PropTipoPredeterminado
emptyMessage
ReactNode"No results."
loading
booleanfalse
loadingRowsNúmero de filas skeleton durante la carga.
number5
pinStartIds de columna que se anclan al borde inicial.
string[]["select", firstColumnId]
stickyHeaderNecesita una altura máxima en el contenedor.
booleanfalse
containerClassNameSe aplica al contenedor de desplazamiento.
string–
swipeSelectShift-arrastra sobre los checkboxes para seleccionar un rango.
booleantrue
classNameSe aplica al elemento table.
string–
AtributoDescripción
data-slot="table-container"El contenedor de desplazamiento.
data-scrolled-startPresente en el contenedor cuando se ha desplazado lejos del borde inicial.
data-scrolled-endPresente mientras queda más por desplazar hacia el borde final.
data-scrolled-topPresente una vez que las filas se desplazan verticalmente.
data-swipe-selectingPresente en el contenedor durante un Shift-arrastre.
data-state="selected"Presente en las filas seleccionadas.
data-row-idEl id de fila de getRowId.
data-slot="data-table-loading-row"Cada fila skeleton.
data-slot="data-table-empty"La fila vacía.

Una fila que se ajusta para la búsqueda, el menú View y tus propios filtros. Acepta todas las props de div y lleva data-table-toolbar como su data-slot.

PropTipoPredeterminado
placeholder
string"Search…"
aria-label
string"Search table"
clearLabelNombre accesible del botón de borrar.
string"Clear search"
AtributoDescripción
data-slot="data-table-search"El contenedor del campo de búsqueda.
PropTipoPredeterminado
label
string"View"
groupLabel
string"Toggle columns"
getLabelPor defecto es el encabezado de tipo cadena de la columna, o su id en mayúscula inicial.
(column) => string–

Lista todas las columnas que se pueden ocultar. Define enableHiding: false en una columna para dejarla fuera.

AtributoDescripción
data-slot="data-table-view-options"El popup del menú.
PropTipoPredeterminado
column
Column–
title
string–

Renderiza un botón de orden para las columnas ordenables y texto simple para el resto.

AtributoDescripción
data-slot="data-table-column-header"El contenedor del encabezado.
data-sortedPresente en el botón de orden mientras la columna está ordenada.
aria-sortEn la celda de encabezado: ascendente, descendente o ninguno.
PropTipoPredeterminado
table
Table–
aria-label
string"Select all rows on this page"

Selecciona las filas de la página actual y muestra un estado indeterminado cuando solo algunas están seleccionadas.

PropTipoPredeterminado
row
Row–
aria-label
string"Select row"
PropTipoPredeterminado
pageSizes
number[][10, 20, 50, 100]
showSelectionMuestra el número de seleccionados en lugar del número de filas.
booleantrue
labelsrowsPerPage, firstPage, previousPage, nextPage y lastPage.
Partial<DataTablePaginationLabels>–
formatSelection
(selected: number, total: number) => ReactNode"2 of 42 rows selected"
formatRows
(total: number) => ReactNode"42 rows"
formatPage
(page: number, pageCount: number) => ReactNode"Page 1 of 5"
AtributoDescripción
data-slot="data-table-pagination"La barra de paginación.

Devuelve la tabla del <DataTable /> más cercano. Úsalo para construir tus propios controles de barra de herramientas, como un filtro de estado.

PropTipoPredeterminado
stickyHeaderAncla la fila de encabezado dentro de un contenedor con desplazamiento.
booleanfalse
containerClassNameSe aplica al contenedor de desplazamiento.
string–
containerRef
Ref<HTMLDivElement>–
AtributoDescripción
data-slot="table"El elemento table.
data-sticky-headerPresente en el contenedor cuando stickyHeader está activado.
--table-bgFondo de la fila y de las celdas ancladas. Sigue a la tarjeta o al popover en que se encuentra.
PropTipoPredeterminado
alignLas celdas alineadas al final también usan números tabulares.
"start" | "center" | "end""start"
pinnedMantiene la celda en su sitio mientras la tabla se desplaza hacia los lados. Se desplaza con --pin-offset.
"start" | "end"–
pinnedEdgeDibuja una sombra suave en la última columna anclada mientras se desplaza.
booleanfalse
AtributoDescripción
data-alignLa alineación actual.
data-pinnedstart o end cuando está anclado.
data-pinned-edgePresente en la última celda anclada de un lado.
--pin-offsetDistancia desde el borde anclado, definida por ti.

TableHeader, TableBody, TableFooter, TableRow y TableCaption renderizan los elementos de tabla correspondientes y aceptan todas sus props.