# Advanced Data Table
*URL: https://www.grettech.com/components/data-table-advanced*

A highly interactive data table with sorting, filtering, row selection, pagination, and bulk actions.

- Global search and filtering
- Column sorting
- Row selection and bulk actions
- Pagination controls
- Responsive layout

## Install with the shadcn CLI
```bash
npx shadcn@latest add https://www.grettech.com/api/registry/data-table-advanced
```
With its demo: `npx shadcn@latest add https://www.grettech.com/api/registry/advanced-data-table-demo`

**Dependencies:** npm: clsx, tailwind-merge, @tanstack/react-table@^8, lucide-react, class-variance-authority; shadcn/ui: button, checkbox, input, table, dropdown-menu, tooltip, select

## Manual installation
**Step 1: Install dependencies**
Install the packages this component uses and the shadcn/ui components it builds on. It also needs the `cn` helper in `lib/utils.ts`, which `npx shadcn@latest init` adds.

```bash
npm install clsx tailwind-merge @tanstack/react-table@^8 lucide-react class-variance-authority
npx shadcn@latest add button checkbox input table dropdown-menu tooltip select
```

**Step 2: Copy the source code**
Add these files to your project components directory.

File: `components/ui/custom-badge.tsx`
```tsx
import { cn } from "@/lib/utils"
import { cva, type VariantProps } from "class-variance-authority"
import * as React from "react"
const badgeVariants = cva(
  "inline-flex items-center rounded-md border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2",
  {
    variants: {
      variant: {
        default: "border-transparent bg-primary text-primary-foreground shadow hover:bg-primary/80",
        secondary: "border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80",
        destructive: "border-transparent bg-destructive text-white shadow hover:bg-destructive/80",
        outline: "text-foreground",
      },
    },
    defaultVariants: {
      variant: "default",
    },
  }
)
export interface BadgeProps extends React.HTMLAttributes<HTMLDivElement>, VariantProps<typeof badgeVariants> {}
function Badge({ className, variant, ...props }: BadgeProps) {
  return <div className={cn(badgeVariants({ variant }), className)} {...props} />
}
export { Badge, badgeVariants }
```

File: `components/ui/data-table-advanced.tsx`
```tsx
"use client"

import { cn } from "@/lib/utils"
import {
  Column,
  ColumnDef,
  ColumnFiltersState,
  flexRender,
  getCoreRowModel,
  getFilteredRowModel,
  getPaginationRowModel,
  getSortedRowModel,
  RowSelectionState,
  SortingState,
  useReactTable as useTableCore,
  VisibilityState,
  Table as ReactTableInstance,
} from "@tanstack/react-table"
import {
  ArrowDown,
  ArrowUp,
  ArrowUpDown,
  ChevronDown,
  ChevronLeft,
  ChevronRight,
  ChevronsLeft,
  ChevronsRight,
  Columns3,
  Search,
  X,
} from "lucide-react"
import * as React from "react"

import { Button } from "@/components/ui/button"
import { Checkbox } from "@/components/ui/checkbox"
import { Input } from "@/components/ui/input"
import {
  Table,
  TableBody,
  TableCell,
  TableHead,
  TableHeader,
  TableRow,
} from "@/components/ui/table"

import {
  DropdownMenu,
  DropdownMenuCheckboxItem,
  DropdownMenuContent,
  DropdownMenuGroup,
  DropdownMenuItem,
  DropdownMenuLabel,
  DropdownMenuSeparator,
  DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu"
import {
  Tooltip,
  TooltipContent,
  TooltipProvider,
  TooltipTrigger,
} from "@/components/ui/tooltip"

// ─── Sortable Header ────────────────────────────────────────────────

export function SortableHeader<TData, TValue>({
  column,
  title,
  className,
}: {
  column: Column<TData, TValue>
  title: string
  className?: string
}) {
  const sorted = column.getIsSorted()

  return (
    <Button
      variant="ghost"
      className={cn(
        "group/sort -ml-2 h-8 gap-1 px-2 text-muted-foreground transition-colors hover:bg-muted/30 hover:text-foreground",
        sorted && "text-foreground",
        className
      )}
      onClick={() => column.toggleSorting(sorted === "asc")}
    >
      <span className="text-xs font-semibold tracking-wider uppercase">
        {title}
      </span>
      <span className="relative flex h-4 w-4 items-center justify-center">
        {sorted === "asc" ? (
          <ArrowUp className="h-3.5 w-3.5 text-primary transition-transform" />
        ) : sorted === "desc" ? (
          <ArrowDown className="h-3.5 w-3.5 text-primary transition-transform" />
        ) : (
          <ArrowUpDown className="h-3.5 w-3.5 opacity-40 transition-opacity group-hover/sort:opacity-70" />
        )}
      </span>
    </Button>
  )
}

// ─── Skeleton Row ───────────────────────────────────────────────────

function SkeletonRow({ columns }: { columns: number }) {
  const widths = [60, 45, 75, 55, 85, 40, 70]
  return (
    <TableRow className="border-border/20 hover:bg-transparent">
      {Array.from({ length: columns }).map((_, i) => (
        <TableCell key={i} className="py-3">
          <div
            className="h-4 animate-pulse rounded-md bg-muted/40"
            style={{
              width: `${widths[i % widths.length]}%`,
              animationDelay: `${i * 75}ms`,
            }}
          />
        </TableCell>
      ))}
    </TableRow>
  )
}

// ─── DataTable Props ────────────────────────────────────────────────

export interface ServerPaginationProps {
  /** Current page (1-indexed) */
  page: number
  /** Total number of pages */
  totalPages: number
  /** Total number of rows across all pages */
  totalRows: number
  /** Callback when page changes */
  onPageChange: (page: number) => void
  /** Page size */
  pageSize: number
  /** Callback when page size changes */
  onPageSizeChange?: (pageSize: number) => void
  /** Whether controls should be disabled (e.g., during loading) */
  disabled?: boolean
}

export interface DataTableProps<TData, TValue> {
  columns: ColumnDef<TData, TValue>[]
  data: TData[]
  loading?: boolean
  noDataMessage?: React.ReactNode
  onRowClick?: (row: TData) => void

  /** Enable row selection checkboxes */
  enableRowSelection?: boolean
  /** Callback when selection changes — receives selected row data */
  onSelectionChange?: (selectedRows: TData[]) => void

  /** Enable built-in global search filter */
  enableSearch?: boolean
  /** Placeholder text for the search input */
  searchPlaceholder?: string
  /** Controlled search value — when provided, search is externally managed (e.g., server-side) */
  searchValue?: string
  /** Callback when search input changes — use with searchValue for controlled/server-side search */
  onSearchChange?: (value: string) => void
  /** Controlled search column — when provided, search is restricted to this column */
  searchColumn?: string
  /** Callback when search column changes */
  onSearchColumnChange?: (column: string) => void
  /** Optional dictionary of hints for specific columns */
  searchHints?: Record<string, string>

  /** Enable client-side pagination with page size selector */
  enablePagination?: boolean
  /** Default page size (defaults to 5) */
  defaultPageSize?: number
  /** Page size options shown in selector (defaults to [5, 10, 20]) */
  pageSizeOptions?: number[]

  /** Server-side pagination — pass this instead of enablePagination for externally controlled paging */
  serverPagination?: ServerPaginationProps

  /** Content shown before the column toggle in the toolbar (e.g., filter controls) */
  toolbarPrefix?: React.ReactNode
  /** Content shown after the column toggle in the toolbar (e.g., action buttons) */
  toolbarSuffix?: React.ReactNode

  /** Number of skeleton rows shown during loading (defaults to 5) */
  skeletonRows?: number
}

// ─── Sub-components ─────────────────────────────────────────────────

const formatColumnName = (id: string) =>
  id
    .replace(/_id$/, "")
    .replace(/_/g, " ")
    .replace(/\b\w/g, (l) => l.toUpperCase())

const DataTableToolbar = <TData,>({
  table,
  enableSearch,
  searchPlaceholder,
  searchDisplayValue,
  setSearchDisplayValue,
  activeSearchColumn,
  handleSearchColumnChange,
  searchHints,
  toolbarPrefix,
  toolbarSuffix,
  enableRowSelection,
}: {
  table: ReactTableInstance<TData>
  enableSearch: boolean
  searchPlaceholder: string
  searchDisplayValue: string
  setSearchDisplayValue: (val: string) => void
  activeSearchColumn: string
  handleSearchColumnChange: (col: string) => void
  searchHints?: Record<string, string>
  toolbarPrefix?: React.ReactNode
  toolbarSuffix?: React.ReactNode
  enableRowSelection: boolean
}) => {
  const selectedCount = table.getSelectedRowModel().rows.length

  const getColumnHint = (columnId: string) => {
    if (searchHints && searchHints[columnId]) {
      return searchHints[columnId]
    }
    return ""
  }

  const getDynamicPlaceholder = () => {
    if (!activeSearchColumn || activeSearchColumn === "all")
      return searchPlaceholder || "Search..."
    const colName = formatColumnName(
      table.getColumn(activeSearchColumn)?.id || activeSearchColumn
    )
    const hint = getColumnHint(activeSearchColumn)
    if (hint) return `Search ${colName} (${hint})...`
    return `Search ${colName}...`
  }

  return (
    <div className="flex w-full flex-col items-start justify-between gap-3 xl:flex-row xl:items-end">
      {/* Left side: search + prefix */}
      <div className="flex w-full min-w-0 flex-1 flex-wrap items-end gap-3">
        {enableSearch && (
          <div className="flex h-9 w-full max-w-sm items-center overflow-hidden rounded-lg border border-border/30 bg-input/50 transition-shadow focus-within:ring-1 focus-within:ring-ring">
            <div className="flex-shrink-0 pr-1 pl-3 text-muted-foreground">
              <Search className="h-4 w-4" />
            </div>
            <div className="relative h-full flex-1">
              <input
                placeholder={getDynamicPlaceholder()}
                value={searchDisplayValue}
                onChange={(e) => setSearchDisplayValue(e.target.value)}
                className="h-full w-full border-none bg-transparent px-2 text-sm outline-none placeholder:text-muted-foreground"
              />
              {searchDisplayValue && (
                <button
                  onClick={() => setSearchDisplayValue("")}
                  className="absolute top-1/2 right-2 -translate-y-1/2 text-muted-foreground transition-colors hover:text-foreground"
                >
                  <X className="h-3.5 w-3.5" />
                </button>
              )}
            </div>
            <div className="h-full border-l border-border/30">
              <DropdownMenu>
                <DropdownMenuTrigger asChild>
                  <Button
                    variant="ghost"
                    className="flex h-full gap-1 rounded-none bg-transparent px-3 text-xs font-normal text-muted-foreground hover:text-foreground"
                  >
                    {activeSearchColumn
                      ? activeSearchColumn === "all"
                        ? "All Columns"
                        : formatColumnName(
                            table.getColumn(activeSearchColumn)?.id ||
                              activeSearchColumn
                          )
                      : "Search In..."}
                    <ChevronDown className="h-3 w-3 opacity-50" />
                  </Button>
                </DropdownMenuTrigger>
                <DropdownMenuContent
                  align="end"
                  className="glass-strong min-w-[140px] border-border/30"
                >
                  <DropdownMenuGroup>
                    <DropdownMenuItem
                      onClick={() => handleSearchColumnChange("all")}
                    >
                      All Columns
                    </DropdownMenuItem>
                    {table
                      .getAllColumns()
                      .filter(
                        (col) =>
                          col.getCanFilter() &&
                          col.id !== "select" &&
                          col.id !== "actions"
                      )
                      .map((col) => {
                        const hint = getColumnHint(col.id)
                        return (
                          <DropdownMenuItem
                            key={col.id}
                            onClick={() => handleSearchColumnChange(col.id)}
                          >
                            {!hint ? (
                              formatColumnName(col.id)
                            ) : (
                              <TooltipProvider>
                                <Tooltip>
                                  <TooltipTrigger asChild>
                                    <div className="w-full text-left">
                                      {formatColumnName(col.id)}
                                    </div>
                                  </TooltipTrigger>
                                  <TooltipContent
                                    side="right"
                                    sideOffset={15}
                                    className="border border-border/50 px-2 py-1 font-mono text-[10px]"
                                  >
                                    {hint}
                                  </TooltipContent>
                                </Tooltip>
                              </TooltipProvider>
                            )}
                          </DropdownMenuItem>
                        )
                      })}
                  </DropdownMenuGroup>
                </DropdownMenuContent>
              </DropdownMenu>
            </div>
          </div>
        )}
        {toolbarPrefix}
      </div>

      {/* Right side: suffix + column toggle */}
      <div className="ml-auto flex items-center gap-2">
        {enableRowSelection && selectedCount > 0 && (
          <div className="animate-fade-in flex items-center gap-2 rounded-lg border border-primary/20 bg-primary/10 px-3 py-1.5 text-xs font-medium text-primary">
            <span className="flex h-5 w-5 items-center justify-center rounded-full bg-primary text-[10px] font-bold text-primary-foreground">
              {selectedCount}
            </span>
            selected
          </div>
        )}
        {toolbarSuffix}
        <DropdownMenu>
          <DropdownMenuTrigger className="glass-strong inline-flex h-9 items-center justify-center gap-1.5 rounded-lg border border-border/30 bg-input/50 px-3 py-2 text-sm font-medium whitespace-nowrap transition-colors hover:bg-accent hover:text-accent-foreground">
            <Columns3 className="h-4 w-4" />
            <span className="hidden sm:inline">Columns</span>
            <ChevronDown className="h-3.5 w-3.5 opacity-50" />
          </DropdownMenuTrigger>
          <DropdownMenuContent
            align="end"
            className="glass-strong w-44 border-border/30"
          >
            <DropdownMenuGroup>
              <DropdownMenuLabel className="text-xs text-muted-foreground">
                Toggle columns
              </DropdownMenuLabel>
              <DropdownMenuSeparator className="bg-border/20" />
              {table
                .getAllColumns()
                .filter((column) => column.getCanHide())
                .map((column) => {
                  return (
                    <DropdownMenuCheckboxItem
                      key={column.id}
                      className="text-sm capitalize"
                      checked={column.getIsVisible()}
                      onCheckedChange={(value) =>
                        column.toggleVisibility(!!value)
                      }
                    >
                      {formatColumnName(column.id)}
                    </DropdownMenuCheckboxItem>
                  )
                })}
            </DropdownMenuGroup>
          </DropdownMenuContent>
        </DropdownMenu>
      </div>
    </div>
  )
}

const DataTableContent = <TData, TValue>({
  table,
  columns,
  loading,
  skeletonRows,
  onRowClick,
  noDataMessage,
  searchDisplayValue,
  setSearchDisplayValue,
}: {
  table: ReactTableInstance<TData>
  columns: ColumnDef<TData, TValue>[]
  loading: boolean
  skeletonRows: number
  onRowClick?: (row: TData) => void
  noDataMessage: React.ReactNode
  searchDisplayValue: string
  setSearchDisplayValue: (val: string) => void
}) => {
  return (
    <div className="glass overflow-hidden rounded-xl">
      <Table>
        <TableHeader className="sticky top-0 z-10 bg-muted backdrop-blur-md">
          {table.getHeaderGroups().map((headerGroup) => (
            <TableRow
              key={headerGroup.id}
              className="border-border/30 hover:bg-transparent"
            >
              {headerGroup.headers.map((header) => {
                return (
                  <TableHead
                    key={header.id}
                    className="h-11 whitespace-nowrap text-muted-foreground"
                    style={{ width: header.getSize() }}
                  >
                    {header.isPlaceholder
                      ? null
                      : flexRender(
                          header.column.columnDef.header,
                          header.getContext()
                        )}
                  </TableHead>
                )
              })}
            </TableRow>
          ))}
        </TableHeader>
        <TableBody>
          {loading ? (
            Array.from({ length: skeletonRows }).map((_, i) => (
              <SkeletonRow key={i} columns={columns.length} />
            ))
          ) : table.getRowModel().rows?.length ? (
            table.getRowModel().rows.map((row) => (
              <TableRow
                key={row.id}
                data-state={row.getIsSelected() && "selected"}
                className={cn(
                  "border-border/20 transition-colors",
                  row.getIsSelected()
                    ? "bg-primary/5 hover:bg-primary/8"
                    : "hover:bg-muted/10",
                  onRowClick && "cursor-pointer"
                )}
                onClick={() => onRowClick && onRowClick(row.original)}
              >
                {row.getVisibleCells().map((cell) => (
                  <TableCell key={cell.id}>
                    {flexRender(cell.column.columnDef.cell, cell.getContext())}
                  </TableCell>
                ))}
              </TableRow>
            ))
          ) : (
            <TableRow className="border-border/20">
              <TableCell colSpan={columns.length} className="py-16 text-center">
                <div className="flex flex-col items-center">
                  <div className="mb-3 flex h-14 w-14 items-center justify-center rounded-xl border border-border/50 bg-muted/30">
                    <svg
                      width="24"
                      height="24"
                      viewBox="0 0 24 24"
                      fill="none"
                      stroke="currentColor"
                      strokeWidth="1.5"
                      className="text-muted-foreground"
                    >
                      <path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z" />
                      <line x1="9" y1="10" x2="15" y2="10" />
                    </svg>
                  </div>
                  <p className="text-sm text-muted-foreground">
                    {noDataMessage}
                  </p>
                  {searchDisplayValue && (
                    <button
                      onClick={() => setSearchDisplayValue("")}
                      className="mt-2 text-xs text-primary transition-colors hover:text-primary/80"
                    >
                      Clear search
                    </button>
                  )}
                </div>
              </TableCell>
            </TableRow>
          )}
        </TableBody>
      </Table>
    </div>
  )
}

const DataTablePagination = <TData,>({
  table,
  enablePagination,
  enableRowSelection,
  serverPagination,
  pageSizeOptions,
}: {
  table: ReactTableInstance<TData>
  enablePagination: boolean
  enableRowSelection: boolean
  serverPagination?: ServerPaginationProps
  pageSizeOptions: number[]
}) => {
  const selectedCount = table.getSelectedRowModel().rows.length
  const totalRows = table.getFilteredRowModel().rows.length

  return (
    <div className="flex flex-col items-center justify-between gap-3 px-1 sm:flex-row">
      {/* Left: selection / total info */}
      <div className="order-2 text-xs text-muted-foreground sm:order-1">
        {enableRowSelection ? (
          <>
            {selectedCount} of{" "}
            {serverPagination ? serverPagination.totalRows : totalRows} row(s)
            selected
          </>
        ) : enablePagination || serverPagination ? (
          <>
            {serverPagination
              ? serverPagination.totalRows.toLocaleString()
              : totalRows}{" "}
            row(s) total
          </>
        ) : null}
      </div>

      {/* Right: pagination controls — client-side */}
      {enablePagination && !serverPagination && (
        <div className="order-1 flex flex-wrap items-center justify-center gap-2 sm:order-2 sm:gap-4">
          {/* Rows per page */}
          <div className="flex items-center gap-2">
            <span className="hidden text-xs whitespace-nowrap text-muted-foreground sm:inline">
              Rows per page
            </span>
            <DropdownMenu>
              <DropdownMenuTrigger asChild>
                <Button
                  variant="outline"
                  size="sm"
                  className="flex h-8 min-w-[68px] justify-between gap-2 bg-input/50 px-3 text-xs"
                >
                  {table.getState().pagination.pageSize}
                  <ChevronDown className="h-3.5 w-3.5 opacity-50" />
                </Button>
              </DropdownMenuTrigger>
              <DropdownMenuContent
                align="end"
                className="glass-strong w-[120px] border-border/30"
              >
                <div className="p-1.5">
                  <Input
                    type="text"
                    className="h-7 bg-input/50 text-xs"
                    placeholder="Custom..."
                    onKeyDown={(e) => {
                      e.stopPropagation()
                      if (e.key === "Enter") {
                        const val = Number(e.currentTarget.value)
                        if (val > 0) table.setPageSize(val)
                      }
                    }}
                  />
                </div>
                <DropdownMenuSeparator />
                {pageSizeOptions.map((size) => (
                  <DropdownMenuItem
                    key={size}
                    onClick={() => table.setPageSize(size)}
                  >
                    {size}
                  </DropdownMenuItem>
                ))}
              </DropdownMenuContent>
            </DropdownMenu>
          </div>

          {/* Page indicator */}
          <span className="text-xs font-medium whitespace-nowrap text-muted-foreground">
            Page {table.getState().pagination.pageIndex + 1} of{" "}
            {table.getPageCount()}
          </span>

          {/* Page navigation */}
          <div className="flex items-center gap-1">
            <Button
              variant="outline"
              size="icon"
              onClick={() => table.setPageIndex(0)}
              disabled={!table.getCanPreviousPage()}
              className="h-7 w-7 bg-input/30"
            >
              <ChevronsLeft className="h-3.5 w-3.5" />
            </Button>
            <Button
              variant="outline"
              size="icon"
              onClick={() => table.previousPage()}
              disabled={!table.getCanPreviousPage()}
              className="h-7 w-7 bg-input/30"
            >
              <ChevronLeft className="h-3.5 w-3.5" />
            </Button>
            <Button
              variant="outline"
              size="icon"
              onClick={() => table.nextPage()}
              disabled={!table.getCanNextPage()}
              className="h-7 w-7 bg-input/30"
            >
              <ChevronRight className="h-3.5 w-3.5" />
            </Button>
            <Button
              variant="outline"
              size="icon"
              onClick={() => table.setPageIndex(table.getPageCount() - 1)}
              disabled={!table.getCanNextPage()}
              className="h-7 w-7 bg-input/30"
            >
              <ChevronsRight className="h-3.5 w-3.5" />
            </Button>
          </div>
        </div>
      )}

      {/* Right: pagination controls — server-side */}
      {serverPagination && (
        <div className="order-1 flex flex-wrap items-center justify-center gap-2 sm:order-2 sm:gap-4">
          {/* Rows per page */}
          {serverPagination.onPageSizeChange && (
            <div className="flex items-center gap-2">
              <span className="hidden text-xs whitespace-nowrap text-muted-foreground sm:inline">
                Rows per page
              </span>
              <DropdownMenu>
                <DropdownMenuTrigger asChild>
                  <Button
                    variant="outline"
                    size="sm"
                    className="flex h-8 min-w-[68px] justify-between gap-2 bg-input/50 px-3 text-xs"
                  >
                    {serverPagination.pageSize}
                    <ChevronDown className="h-3.5 w-3.5 opacity-50" />
                  </Button>
                </DropdownMenuTrigger>
                <DropdownMenuContent
                  align="end"
                  className="glass-strong w-[120px] border-border/30"
                >
                  <div className="p-1.5">
                    <Input
                      type="text"
                      className="h-7 bg-input/50 text-xs"
                      placeholder="Custom..."
                      onKeyDown={(e) => {
                        e.stopPropagation()
                        if (e.key === "Enter") {
                          const val = Number(e.currentTarget.value)
                          if (val > 0) serverPagination.onPageSizeChange?.(val)
                        }
                      }}
                    />
                  </div>
                  <DropdownMenuSeparator />
                  {pageSizeOptions.map((size) => (
                    <DropdownMenuItem
                      key={size}
                      onClick={() => serverPagination.onPageSizeChange?.(size)}
                    >
                      {size}
                    </DropdownMenuItem>
                  ))}
                </DropdownMenuContent>
              </DropdownMenu>
            </div>
          )}

          {/* Page indicator */}
          <span className="text-xs font-medium whitespace-nowrap text-muted-foreground">
            Page {serverPagination.page} of {serverPagination.totalPages}
          </span>

          {/* Page navigation */}
          <div className="flex items-center gap-1">
            <Button
              variant="outline"
              size="icon"
              onClick={() => serverPagination.onPageChange(1)}
              disabled={serverPagination.page <= 1 || serverPagination.disabled}
              className="h-7 w-7 bg-input/30"
            >
              <ChevronsLeft className="h-3.5 w-3.5" />
            </Button>
            <Button
              variant="outline"
              size="icon"
              onClick={() =>
                serverPagination.onPageChange(serverPagination.page - 1)
              }
              disabled={serverPagination.page <= 1 || serverPagination.disabled}
              className="h-7 w-7 bg-input/30"
            >
              <ChevronLeft className="h-3.5 w-3.5" />
            </Button>
            <Button
              variant="outline"
              size="icon"
              onClick={() =>
                serverPagination.onPageChange(serverPagination.page + 1)
              }
              disabled={
                serverPagination.page >= serverPagination.totalPages ||
                serverPagination.disabled
              }
              className="h-7 w-7 bg-input/30"
            >
              <ChevronRight className="h-3.5 w-3.5" />
            </Button>
            <Button
              variant="outline"
              size="icon"
              onClick={() =>
                serverPagination.onPageChange(serverPagination.totalPages)
              }
              disabled={
                serverPagination.page >= serverPagination.totalPages ||
                serverPagination.disabled
              }
              className="h-7 w-7 bg-input/30"
            >
              <ChevronsRight className="h-3.5 w-3.5" />
            </Button>
          </div>
        </div>
      )}
    </div>
  )
}

// ─── Main Component ──────────────────────────────────────────────────

export function DataTable<TData, TValue>({
  columns,
  data,
  loading = false,
  noDataMessage = "No results.",
  onRowClick,
  enableRowSelection = true,
  onSelectionChange,
  enableSearch = true,
  searchPlaceholder = "Search...",
  searchValue,
  onSearchChange,
  enablePagination = true,
  defaultPageSize = 5,
  pageSizeOptions = [5, 10, 20],
  serverPagination,
  toolbarPrefix,
  toolbarSuffix,
  skeletonRows = 5,
  searchColumn,
  onSearchColumnChange,
  searchHints,
}: DataTableProps<TData, TValue>) {
  const isControlledSearch = searchValue !== undefined
  const [sorting, setSorting] = React.useState<SortingState>([])
  const [columnFilters, setColumnFilters] = React.useState<ColumnFiltersState>(
    []
  )
  const [columnVisibility, setColumnVisibility] =
    React.useState<VisibilityState>({})
  const [rowSelection, setRowSelection] = React.useState<RowSelectionState>({})
  const [globalFilter, setGlobalFilter] = React.useState("")
  const [internalSearchValue, setInternalSearchValue] = React.useState("")
  const [internalSearchColumn, setInternalSearchColumn] = React.useState("all")

  const activeSearchColumn =
    searchColumn !== undefined ? searchColumn : internalSearchColumn
  const searchDisplayValue = isControlledSearch
    ? searchValue
    : internalSearchValue

  const setSearchDisplayValue = React.useCallback(
    (value: string) => {
      if (isControlledSearch && onSearchChange) {
        onSearchChange(value)
      } else {
        setInternalSearchValue(value)
      }
    },
    [isControlledSearch, onSearchChange]
  )

  const handleSearchColumnChange = React.useCallback(
    (col: string) => {
      if (onSearchColumnChange) {
        onSearchColumnChange(col)
      } else {
        setInternalSearchColumn(col)
      }
    },
    [onSearchColumnChange]
  )

  React.useEffect(() => {
    if (isControlledSearch) return

    if (activeSearchColumn === "all") {
      setColumnFilters([])
      setGlobalFilter(internalSearchValue)
    } else {
      setGlobalFilter("")
      setColumnFilters(
        internalSearchValue
          ? [{ id: activeSearchColumn, value: internalSearchValue }]
          : []
      )
    }
  }, [activeSearchColumn, internalSearchValue, isControlledSearch])

  const table = useTableCore({
    data,
    columns,
    getCoreRowModel: getCoreRowModel(),
    onSortingChange: setSorting,
    getSortedRowModel: getSortedRowModel(),
    onColumnFiltersChange: setColumnFilters,
    getFilteredRowModel: getFilteredRowModel(),
    onGlobalFilterChange: setGlobalFilter,
    globalFilterFn: (row, columnId, filterValue) => {
      const value = row.getValue(columnId)
      if (value == null) return false
      const strVal = String(value).toLowerCase()
      const filterStr = String(filterValue).toLowerCase()
      if (strVal.includes(filterStr)) return true
      if (typeof value === "number") {
        const formatted = value.toFixed(2)
        if (formatted.includes(filterStr)) return true
        const currency = new Intl.NumberFormat("en-US", {
          style: "currency",
          currency: "USD",
        })
          .format(value)
          .toLowerCase()
        if (currency.includes(filterStr)) return true
      }
      return false
    },
    onColumnVisibilityChange: setColumnVisibility,
    onRowSelectionChange: setRowSelection,
    enableRowSelection,
    // TanStack's auto-reset queues a state update in a microtask during
    // render, which can fire before the table mounts ("Can't perform a React
    // state update on a component that hasn't mounted yet"). The effect below
    // does the same reset after mount instead.
    autoResetPageIndex: false,
    ...(enablePagination
      ? { getPaginationRowModel: getPaginationRowModel() }
      : {}),
    state: {
      sorting,
      columnFilters,
      columnVisibility,
      rowSelection,
      globalFilter: isControlledSearch ? undefined : globalFilter,
    },
    initialState: enablePagination
      ? { pagination: { pageSize: defaultPageSize } }
      : undefined,
  })

  // Back to the first page when the rows change (new data, search, filters
  // or sorting), as TanStack's auto-reset did, but only once mounted.
  const hasMounted = React.useRef(false)
  React.useEffect(() => {
    if (!hasMounted.current) {
      hasMounted.current = true
      return
    }
    table.setPageIndex(0)
  }, [data, globalFilter, columnFilters, sorting, table])

  React.useEffect(() => {
    if (onSelectionChange) {
      const selectedRows = table
        .getSelectedRowModel()
        .rows.map((row) => row.original)
      onSelectionChange(selectedRows)
    }
  }, [rowSelection, onSelectionChange, table])

  const hasToolbar =
    enableSearch ||
    toolbarPrefix ||
    toolbarSuffix ||
    table.getAllColumns().some((col) => col.getCanHide())

  return (
    <div className="space-y-3">
      {hasToolbar && (
        <DataTableToolbar
          table={table}
          enableSearch={enableSearch}
          searchPlaceholder={searchPlaceholder}
          searchDisplayValue={searchDisplayValue}
          setSearchDisplayValue={setSearchDisplayValue}
          activeSearchColumn={activeSearchColumn}
          handleSearchColumnChange={handleSearchColumnChange}
          searchHints={searchHints}
          toolbarPrefix={toolbarPrefix}
          toolbarSuffix={toolbarSuffix}
          enableRowSelection={enableRowSelection}
        />
      )}

      <DataTableContent
        table={table}
        columns={columns}
        loading={loading}
        skeletonRows={skeletonRows}
        onRowClick={onRowClick}
        noDataMessage={noDataMessage}
        searchDisplayValue={searchDisplayValue}
        setSearchDisplayValue={setSearchDisplayValue}
      />

      {(enablePagination || serverPagination || enableRowSelection) &&
        !loading && (
          <DataTablePagination
            table={table}
            enablePagination={enablePagination}
            enableRowSelection={enableRowSelection}
            serverPagination={serverPagination}
            pageSizeOptions={pageSizeOptions}
          />
        )}
    </div>
  )
}

// ─── Selection Column Helper ────────────────────────────────────────

export function getSelectionColumn<TData>(): ColumnDef<TData, unknown> {
  return {
    id: "select",
    header: ({ table }) => (
      <div className="px-1">
        {/* A plain boolean: Radix and Base UI express "indeterminate"
            differently, and this works with both */}
        <Checkbox
          checked={table.getIsAllPageRowsSelected()}
          onCheckedChange={(value) => table.toggleAllPageRowsSelected(!!value)}
          aria-label="Select all"
          className="border-primary/40 dark:bg-transparent"
        />
      </div>
    ),
    cell: ({ row }) => (
      <div className="px-1" onClick={(e) => e.stopPropagation()}>
        <Checkbox
          checked={row.getIsSelected()}
          onCheckedChange={(value) => row.toggleSelected(!!value)}
          aria-label="Select row"
          className="border-primary/40 dark:bg-transparent"
        />
      </div>
    ),
    enableSorting: false,
    enableHiding: false,
    size: 40,
  }
}

```

**Step 3: Framework Adjustments (Optional)**
If you are manually copying this code into a Vite or Remix project (without using the CLI), make sure to remove the `"use client";` directive from the very top of the file.

## Props
| Name | Type | Default | Required | Description |
|---|---|---|---|---|
| `columns` | `ColumnDef<TData, TValue>[]` | - | Yes | TanStack Table column definitions. Use getSelectionColumn() and SortableHeader from this file for the checkbox column and sortable headers. |
| `data` | `TData[]` | - | Yes | The rows to show. |
| `loading` | `boolean` | `false` | No | Shows skeleton rows instead of data. |
| `skeletonRows` | `number` | `5` | No | Number of skeleton rows shown while loading. |
| `noDataMessage` | `React.ReactNode` | `"No results."` | No | Shown when there are no rows. |
| `onRowClick` | `(row: TData) => void` | - | No | Called with the row's data when a row is clicked. |
| `enableRowSelection` | `boolean` | `true` | No | Allows rows to be selected. Add getSelectionColumn() to columns to show the checkboxes. |
| `onSelectionChange` | `(selectedRows: TData[]) => void` | - | No | Called with the selected rows' data whenever the selection changes. |
| `enableSearch` | `boolean` | `true` | No | Shows the search box and column picker. |
| `searchPlaceholder` | `string` | `"Search..."` | No | Placeholder of the search box while searching all columns. A specific column shows "Search <column>..." instead. |
| `searchValue` | `string` | - | No | Controlled search text. When set, the table stops filtering itself so you can search on the server. |
| `onSearchChange` | `(value: string) => void` | - | No | Called with the new search text. Use with searchValue. |
| `searchColumn` | `string` | - | No | Controlled search column: "all" or a column id. |
| `onSearchColumnChange` | `(column: string) => void` | - | No | Called when a different search column is picked. Use with searchColumn. |
| `searchHints` | `Record<string, string>` | - | No | Example values per column id. Shown as tooltips in the column picker and in the placeholder once that column is picked. |
| `enablePagination` | `boolean` | `true` | No | Client-side pagination with a page size selector. |
| `defaultPageSize` | `number` | `5` | No | Initial number of rows per page. |
| `pageSizeOptions` | `number[]` | `[5, 10, 20]` | No | Choices in the rows-per-page selector. |
| `serverPagination` | `ServerPaginationProps` | - | No | Pass this instead of relying on enablePagination when the server pages the data. |
| `serverPagination.page` | `number` | - | Yes | Current page, starting at 1. |
| `serverPagination.totalPages` | `number` | - | Yes | Total number of pages. |
| `serverPagination.totalRows` | `number` | - | Yes | Total number of rows across all pages. |
| `serverPagination.onPageChange` | `(page: number) => void` | - | Yes | Called with the page to load. |
| `serverPagination.pageSize` | `number` | - | Yes | Rows per page. |
| `serverPagination.onPageSizeChange` | `(pageSize: number) => void` | - | No | Called when a different page size is picked. |
| `serverPagination.disabled` | `boolean` | - | No | Disables the paging controls, e.g. while loading. |
| `toolbarPrefix` | `React.ReactNode` | - | No | Content before the column toggle in the toolbar, e.g. filters. |
| `toolbarSuffix` | `React.ReactNode` | - | No | Content after the column toggle in the toolbar, e.g. action buttons. |

## Usage
```tsx
"use client"

import { ColumnDef } from "@tanstack/react-table"
import { MoreHorizontal } from "lucide-react"
import * as React from "react"

import { Badge } from "@/components/ui/custom-badge"
import { Button } from "@/components/ui/button"

import {
  DropdownMenu,
  DropdownMenuContent,
  DropdownMenuItem,
  DropdownMenuLabel,
  DropdownMenuSeparator,
  DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu"
import {
  Select,
  SelectContent,
  SelectItem,
  SelectTrigger,
  SelectValue,
} from "@/components/ui/select"

// ─── Sortable Header ────────────────────────────────────────────────
import {
  DataTable,
  SortableHeader,
  getSelectionColumn,
} from "@/components/ui/data-table-advanced"

export type DemoData = {
  id: string
  name: string
  email: string
  status: "Active" | "Inactive" | "Pending"
  amount: number
  lastLogin: string
}

const DEMO_DATA: DemoData[] = [
  {
    id: "USR-001",
    name: "John Doe",
    email: "john@example.com",
    status: "Active",
    amount: 1250.0,
    lastLogin: "2 hours ago",
  },
  {
    id: "USR-002",
    name: "Jane Smith",
    email: "jane@example.com",
    status: "Active",
    amount: 450.5,
    lastLogin: "5 mins ago",
  },
  {
    id: "USR-003",
    name: "Bob Johnson",
    email: "bob@example.com",
    status: "Inactive",
    amount: 0.0,
    lastLogin: "2 months ago",
  },
  {
    id: "USR-004",
    name: "Alice Brown",
    email: "alice@example.com",
    status: "Pending",
    amount: 150.0,
    lastLogin: "Never",
  },
  {
    id: "USR-005",
    name: "Charlie Davis",
    email: "charlie@example.com",
    status: "Active",
    amount: 3200.75,
    lastLogin: "1 day ago",
  },
  {
    id: "USR-006",
    name: "Diana Prince",
    email: "diana@example.com",
    status: "Active",
    amount: 890.0,
    lastLogin: "3 hours ago",
  },
]

export default function DataTableAdvancedDemo() {
  const [selectedStatus, setSelectedStatus] = React.useState<
    DemoData["status"] | ""
  >("")
  const [, setSelectedRows] = React.useState<DemoData[]>([])

  const columns = React.useMemo<ColumnDef<DemoData>[]>(
    () => [
      getSelectionColumn<DemoData>(),
      {
        header: ({ column }) => <SortableHeader column={column} title="User" />,
        accessorKey: "name",
        cell: ({ row }) => (
          <div className="flex flex-col">
            <span className="font-medium">{row.original.name}</span>
            <span className="text-xs text-muted-foreground">
              {row.original.email}
            </span>
          </div>
        ),
      },
      {
        header: ({ column }) => (
          <SortableHeader column={column} title="Status" />
        ),
        accessorKey: "status",
        cell: ({ row }) => {
          const status = row.getValue("status") as string
          return (
            <Badge
              variant={
                status === "Active"
                  ? "default"
                  : status === "Inactive"
                    ? "secondary"
                    : "outline"
              }
            >
              {status}
            </Badge>
          )
        },
      },
      {
        header: ({ column }) => (
          <SortableHeader column={column} title="Amount" />
        ),
        accessorKey: "amount",
        filterFn: (row, id, value) => {
          const amount = parseFloat(row.getValue(id))
          const formatted = new Intl.NumberFormat("en-US", {
            style: "currency",
            currency: "USD",
          }).format(amount)
          // Allow searching by raw number string or formatted string
          return (
            formatted.toLowerCase().includes(String(value).toLowerCase()) ||
            String(amount).toLowerCase().includes(String(value).toLowerCase())
          )
        },
        cell: ({ row }) => {
          const amount = parseFloat(row.getValue("amount"))
          const formatted = new Intl.NumberFormat("en-US", {
            style: "currency",
            currency: "USD",
          }).format(amount)
          return <div className="font-medium">{formatted}</div>
        },
      },
      {
        header: ({ column }) => (
          <SortableHeader column={column} title="Last Login" />
        ),
        accessorKey: "lastLogin",
        cell: ({ row }) => (
          <div className="text-sm text-muted-foreground">
            {row.getValue("lastLogin")}
          </div>
        ),
      },
      {
        id: "actions",
        cell: ({ row }) => {
          const user = 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(user.id)}>
                  Copy User ID
                </DropdownMenuItem>
                <DropdownMenuSeparator />
                <DropdownMenuItem>View details</DropdownMenuItem>
                <DropdownMenuItem className="text-destructive">
                  Delete user
                </DropdownMenuItem>
              </DropdownMenuContent>
            </DropdownMenu>
          )
        },
      },
    ],
    []
  )

  const filteredData = React.useMemo(() => {
    if (!selectedStatus) return DEMO_DATA
    return DEMO_DATA.filter((item) => item.status === selectedStatus)
  }, [selectedStatus])

  return (
    <div className="w-full space-y-4">
      <DataTable
        columns={columns}
        data={filteredData}
        enableSearch={true}
        searchPlaceholder="Search users by name..."
        searchHints={{
          status: "Active, Inactive, Pending",
          name: "e.g. John Doe",
          email: "e.g. name@example.com",
          amount: "e.g. 150.00",
          lastLogin: "e.g. 2 hours ago",
        }}
        enablePagination={true}
        defaultPageSize={5}
        pageSizeOptions={[5, 10, 20]}
        enableRowSelection={true}
        onSelectionChange={setSelectedRows}
        toolbarPrefix={
          <div className="flex items-center gap-2">
            <Select
              value={selectedStatus || "all"}
              onValueChange={(val) =>
                setSelectedStatus(
                  val === "all" ? "" : (val as DemoData["status"])
                )
              }
            >
              <SelectTrigger className="h-9 w-[130px] bg-transparent">
                <SelectValue placeholder="All Statuses" />
              </SelectTrigger>
              <SelectContent>
                <SelectItem value="all">All Statuses</SelectItem>
                <SelectItem value="Active">Active</SelectItem>
                <SelectItem value="Inactive">Inactive</SelectItem>
                <SelectItem value="Pending">Pending</SelectItem>
              </SelectContent>
            </Select>
          </div>
        }
      />
    </div>
  )
}

```
