cubby-ui
uidata-tablesource: ok · refreshed
Data Table
A data-table component.
npx shadcn add "https://www.cubby-ui.dev/r/data-table.json"free
Where it goes
data-table
Use cases
dashboard
Tags auto-generated by rules on 9/24/2026 — empty means no evidence, not lack of support.
Dependencies
Bundle weight · light@tanstack/react-table@hugeicons/react@hugeicons/core-free-icons
Registry dependencies
@cubby-ui/checkbox@cubby-ui/toolbar@cubby-ui/table@cubby-ui/dropdown-menu@cubby-ui/button@cubby-ui/input-group@cubby-ui/elevated
Files5 · source cached 10/9/2026, 2:39:49 AM
registry/default/data-table/data-table.tsx
"use client";
import * as React from "react";
import {
useReactTable,
getCoreRowModel,
getSortedRowModel,
getFilteredRowModel,
getPaginationRowModel,
flexRender,
type ColumnDef,
type SortingState,
type RowSelectionState,
type ColumnFiltersState,
type PaginationState,
type VisibilityState,
type OnChangeFn,
type Row,
type Column,
} from "@tanstack/react-table";
import { HugeiconsIcon } from "@hugeicons/react";
import {
ArrowUpDownIcon,
SortByDown01Icon,
SortByUp01Icon,
} from "@hugeicons/core-free-icons";
import { cn } from "@/lib/utils";
import { elevatedSurface } from "@/registry/default/lib/elevated";
import { Checkbox } from "@/registry/default/checkbox/checkbox";
import {
DataTableContext,
useDataTable,
} from "@/registry/default/data-table/data-table-context";
import {
Toolbar,
ToolbarSeparator,
type ToolbarProps,
} from "@/registry/default/toolbar/toolbar";
import {
Table,
TableHeader,
TableBody,
TableFooter,
TableHead,
TableRow,
TableCell,
type TableProps,
} from "@/registry/default/table/table";
import { DataTableSearch } from "@/registry/default/data-table/data-table-search";
import { DataTableColumnToggle } from "@/registry/default/data-table/data-table-column-toggle";
import { DataTablePagination } from "@/registry/default/data-table/data-table-pagination";
export interface DataTableProps<TData, TValue> {
columns: ColumnDef<TData, TValue>[];
data: TData[];
children: React.ReactNode;
className?: string;
enableSorting?: boolean;
enableRowSelection?: boolean | ((row: Row<TData>) => boolean);
enableMultiRowSelection?: boolean;
enableFiltering?: boolean;
enablePagination?: boolean;
showSelectionColumn?: boolean;
sorting?: SortingState;
onSortingChange?: OnChangeFn<SortingState>;
rowSelection?: RowSelectionState;
onRowSelectionChange?: OnChangeFn<RowSelectionState>;
columnFilters?: ColumnFiltersState;
onColumnFiltersChange?: OnChangeFn<ColumnFiltersState>;
pagination?: PaginationState;
onPaginationChange?: OnChangeFn<PaginationState>;
columnVisibility?: VisibilityState;
onColumnVisibilityChange?: OnChangeFn<VisibilityState>;
globalFilter?: string;
onGlobalFilterChange?: OnChangeFn<string>;
getRowId?: (originalRow: TData, index: number, parent?: Row<TData>) => string;
}
function DataTable<TData, TValue>({
columns: userColumns,
data,
children,
className,
enableSorting = false,
enableRowSelection = false,
enableMultiRowSelection = true,
enableFiltering = false,
enablePagination = false,
showSelectionColumn = true,
sorting: controlledSorting,
onSortingChange,
rowSelection: controlledRowSelection,
onRowSelectionChange,
columnFilters: controlledColumnFilters,
onColumnFiltersChange,
pagination: controlledPagination,
onPaginationChange,
columnVisibility: controlledColumnVisibility,
onColumnVisibilityChange,
globalFilter: controlledGlobalFilter,
onGlobalFilterChange,
getRowId,
}: DataTableProps<TData, TValue>) {
const [internalSorting, setInternalSorting] = React.useState<SortingState>(
[],
);
const [internalRowSelection, setInternalRowSelection] =
React.useState<RowSelectionState>({});
const [internalColumnFilters, setInternalColumnFilters] =
React.useState<ColumnFiltersState>([]);
const [internalPagination, setInternalPagination] =
React.useState<PaginationState>({
pageIndex: 0,
pageSize: 10,
});
const [internalGlobalFilter, setInternalGlobalFilter] = React.useState("");
const [internalColumnVisibility, setInternalColumnVisibility] =
React.useState<VisibilityState>({});
const sorting = controlledSorting ?? internalSorting;
const rowSelection = controlledRowSelection ?? internalRowSelection;
const columnFilters = controlledColumnFilters ?? internalColumnFilters;
const pagination = controlledPagination ?? internalPagination;
const globalFilter = controlledGlobalFilter ?? internalGlobalFilter;
const columnVisibility =
controlledColumnVisibility ?? internalColumnVisibility;
const columns = React.useMemo(() => {
if (!enableRowSelection || !showSelectionColumn) {
return userColumns;
}
const selectionColumn: ColumnDef<TData, unknown> = {
id: "__select__",
header: ({ table }) =>
enableMultiRowSelection ? (
<div className="flex items-center">
<Checkbox
checked={table.getIsAllPageRowsSelected()}
indeterminate={
table.getIsSomePageRowsSelected() &&
!table.getIsAllPageRowsSelected()
}
onCheckedChange={(value) =>
table.toggleAllPageRowsSelected(!!value)
}
aria-label="Select all"
/>
</div>
) : null,
cell: ({ row }) => (
<div className="flex items-center">
<Checkbox
checked={row.getIsSelected()}
disabled={!row.getCanSelect()}
onCheckedChange={(value) => row.toggleSelected(!!value)}
aria-label="Select row"
/>
</div>
),
enableSorting: false,
enableHiding: false,
};
return [selectionColumn, ...userColumns];
}, [
userColumns,
enableRowSelection,
enableMultiRowSelection,
showSelectionColumn,
]);
const table = useReactTable({
data,
columns,
getRowId,
state: {
sorting,
rowSelection,
columnFilters,
pagination,
globalFilter,
columnVisibility,
},
onSortingChange: onSortingChange ?? setInternalSorting,
onRowSelectionChange: onRowSelectionChange ?? setInternalRowSelection,
onColumnFiltersChange: onColumnFiltersChange ?? setInternalColumnFilters,
onPaginationChange: onPaginationChange ?? setInternalPagination,
onGlobalFilterChange: onGlobalFilterChange ?? setInternalGlobalFilter,
onColumnVisibilityChange:
onColumnVisibilityChange ?? setInternalColumnVisibility,
getCoreRowModel: getCoreRowModel(),
...(enableSorting && { getSortedRowModel: getSortedRowModel() }),
...(enableFiltering && { getFilteredRowModel: getFilteredRowModel() }),
...(enablePagination && { getPaginationRowModel: getPaginationRowModel() }),
enableRowSelection,
enableMultiRowSelection,
globalFilterFn: "includesString",
});
return (
<DataTableContext.Provider value={{ table }}>
<div
className={cn(
// `relative` required: elevatedSurface's rim ::after must position inside
// this container, not climb to the nearest positioned ancestor.
"relative w-full rounded-2xl md:max-w-2xl",
// pseudo-element rim keeps the border line visible above opaque sticky
// children (DataTableToolbar, table header) in dark mode.
elevatedSurface(3, 1),
"bg-muted",
className,
)}
>
{children}
</div>
</DataTableContext.Provider>
);
}
// A plain row above the table: its controls are fields and triggers, not
// toolbar buttons, so it skips the shell's fixed height and highlight.
function DataTableToolbar({
className,
variant = "ghost",
highlight = false,
...props
}: ToolbarProps) {
return (
<Toolbar
variant={variant}
highlight={highlight}
className={cn(
"w-full items-center gap-1 rounded-b-none px-2 pt-2 pb-0 data-[orientation=horizontal]:h-auto",
className,
)}
{...props}
/>
);
}
export interface DataTableContentProps extends Omit<TableProps, "children"> {
children: React.ReactNode;
}
function DataTableContent({
bordered,
striped,
hoverable,
rowDividers = true,
className,
children,
...tableProps
}: DataTableContentProps) {
return (
<Table
bordered={bordered}
striped={striped}
hoverable={hoverable}
rowDividers={rowDividers}
className={cn(
// Strips Table's own elevation; outer DataTable container owns it.
// `after:hidden` removes the dark-mode rim ::after (would paint a
// square-cornered rim over the DataTable's rounded one).
"rounded-none bg-transparent shadow-none ring-0 after:hidden",
className,
)}
{...tableProps}
>
{children}
</Table>
);
}
function SortableHeader<TData>({
column,
children,
align,
isFirst,
isLast,
}: {
column: Column<TData, unknown>;
children: React.ReactNode;
align?: "left" | "center" | "right";
isFirst?: boolean;
isLast?: boolean;
}) {
return (
<button
type="button"
className={cn(
"group hover:text-foreground -mx-3 -my-2 flex w-[calc(100%+1.5rem)] cursor-pointer items-center gap-1.5 px-3 py-2 transition-colors",
isFirst && "rounded-l-lg",
isLast && "rounded-r-lg",
)}
onClick={() => column.toggleSorting(column.getIsSorted() === "asc")}
>
<span
className={cn(
"flex flex-1 items-center",
align === "right" && "justify-end",
align === "center" && "justify-center",
)}
>
{children}
</span>
{column.getIsSorted() === "asc" ? (
<HugeiconsIcon
icon={SortByUp01Icon}
strokeWidth={2}
className="size-4"
/>
) : column.getIsSorted() === "desc" ? (
<HugeiconsIcon
icon={SortByDown01Icon}
strokeWidth={2}
className="size-4"
/>
) : (
<HugeiconsIcon
icon={ArrowUpDownIcon}
strokeWidth={2}
className="size-4 opacity-0 group-hover:opacity-50"
/>
)}
</button>
);
}
export interface DataTableHeaderProps extends React.ComponentProps<
typeof TableHeader
> {
enableSorting?: boolean;
}
function DataTableHeader({
enableSorting = false,
className,
...props
}: DataTableHeaderProps) {
const { table } = useDataTable();
const renderHeader = (
header: ReturnType<typeof table.getHeaderGroups>[0]["headers"][0],
index: number,
totalHeaders: number,
) => {
if (header.isPlaceholder) return null;
const headerDef = header.column.columnDef.header;
const canSort = header.column.getCanSort();
const meta = header.column.columnDef.meta as
| { align?: "left" | "center" | "right" }
| undefined;
if (typeof headerDef === "string" && enableSorting && canSort) {
return (
<SortableHeader
column={header.column}
align={meta?.align}
isFirst={index === 0}
isLast={index === totalHeaders - 1}
>
{headerDef}
</SortableHeader>
);
}
return flexRender(headerDef, header.getContext());
};
return (
<TableHeader className={className} {...props}>
{table.getHeaderGroups().map((headerGroup) => (
<TableRow key={headerGroup.id}>
{headerGroup.headers.map((header, index) => (
<TableHead key={header.id} colSpan={header.colSpan}>
{renderHeader(header, index, headerGroup.headers.length)}
</TableHead>
))}
</TableRow>
))}
</TableHeader>
);
}
export interface DataTableBodyProps extends React.ComponentProps<
typeof TableBody
> {
emptyState?: React.ReactNode;
}
function DataTableBody({
emptyState,
className,
...props
}: DataTableBodyProps) {
const { table } = useDataTable();
const totalColumns = table.getAllColumns().length;
return (
<TableBody className={className} {...props}>
{table.getRowModel().rows?.length ? (
table.getRowModel().rows.map((row) => (
<TableRow key={row.id} selected={row.getIsSelected()}>
{row.getVisibleCells().map((cell) => (
<TableCell key={cell.id}>
{flexRender(cell.column.columnDef.cell, cell.getContext())}
</TableCell>
))}
</TableRow>
))
) : (
<TableRow>
<TableCell colSpan={totalColumns} className="h-24 text-center">
{emptyState ?? "No results."}
</TableCell>
</TableRow>
)}
</TableBody>
);
}
export type DataTableFooterProps = React.ComponentProps<typeof TableFooter>;
function DataTableFooter({
className,
children,
...props
}: DataTableFooterProps) {
return (
<TableFooter className={className} {...props}>
{children}
</TableFooter>
);
}
export {
DataTable,
DataTableToolbar,
ToolbarSeparator as DataTableToolbarSeparator,
DataTableSearch,
DataTableColumnToggle,
DataTableContent,
DataTableHeader,
DataTableBody,
DataTableFooter,
DataTablePagination,
useDataTable,
};
export type {
ColumnDef,
SortingState,
RowSelectionState,
ColumnFiltersState,
VisibilityState,
PaginationState,
};
registry/default/data-table/data-table-column-toggle.tsx
"use client";
import * as React from "react";
import { HugeiconsIcon } from "@hugeicons/react";
import { FilterHorizontalIcon } from "@hugeicons/core-free-icons";
import {
DropdownMenu,
DropdownMenuTrigger,
DropdownMenuContent,
DropdownMenuLabel,
DropdownMenuCheckboxItem,
} from "@/registry/default/dropdown-menu/dropdown-menu";
import { Button } from "@/registry/default/button/button";
import { useDataTable } from "@/registry/default/data-table/data-table-context";
function DataTableColumnToggle() {
const { table } = useDataTable();
const toggleableColumns = table
.getAllColumns()
.filter((col) => col.id !== "__select__" && col.getCanHide());
return (
<DropdownMenu>
<DropdownMenuTrigger render={<Button variant="ghost" size="icon_sm" />}>
<HugeiconsIcon
icon={FilterHorizontalIcon}
className="size-4"
strokeWidth={2}
/>
</DropdownMenuTrigger>
<DropdownMenuContent side="bottom" align="end">
<DropdownMenuLabel>Toggle columns</DropdownMenuLabel>
{toggleableColumns.map((column) => {
const label =
typeof column.columnDef.header === "string"
? column.columnDef.header
: column.id;
return (
<DropdownMenuCheckboxItem
key={column.id}
checked={column.getIsVisible()}
onCheckedChange={(checked) => column.toggleVisibility(!!checked)}
>
{label}
</DropdownMenuCheckboxItem>
);
})}
</DropdownMenuContent>
</DropdownMenu>
);
}
export { DataTableColumnToggle };
registry/default/data-table/data-table-context.tsx
"use client"
import * as React from "react"
import { type Table } from "@tanstack/react-table"
interface DataTableContextValue<TData> {
table: Table<TData>
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Generic context requires any; type safety is enforced via useDataTable<TData>()
const DataTableContext = React.createContext<DataTableContextValue<any> | null>(
null
)
function useDataTable<TData>() {
const context = React.useContext(DataTableContext)
if (!context) {
throw new Error("useDataTable must be used within a DataTable")
}
return context as DataTableContextValue<TData>
}
export { DataTableContext, useDataTable }
export type { DataTableContextValue }
registry/default/data-table/data-table-pagination.tsx
"use client"
import * as React from "react"
import { cn } from "@/lib/utils"
import { Button } from "@/registry/default/button/button"
import { useDataTable } from "@/registry/default/data-table/data-table-context"
interface DataTablePaginationProps {
className?: string
showSelectedCount?: boolean
}
function DataTablePagination({
className,
showSelectedCount = true,
}: DataTablePaginationProps) {
const { table } = useDataTable()
return (
<div
className={cn(
"flex items-center justify-between px-2 pb-2",
className
)}
>
{showSelectedCount ? (
<span className="text-muted-foreground text-sm">
{table.getFilteredSelectedRowModel().rows.length} of{" "}
{table.getFilteredRowModel().rows.length} row(s) selected
</span>
) : (
<div />
)}
<div className="flex items-center gap-2">
<Button
variant="outline"
size="sm"
onClick={() => table.previousPage()}
disabled={!table.getCanPreviousPage()}
>
Previous
</Button>
<Button
variant="outline"
size="sm"
onClick={() => table.nextPage()}
disabled={!table.getCanNextPage()}
>
Next
</Button>
</div>
</div>
)
}
export { DataTablePagination }
registry/default/data-table/data-table-search.tsx
"use client";
import * as React from "react";
import { Toolbar } from "@base-ui/react/toolbar";
import { HugeiconsIcon } from "@hugeicons/react";
import { Search01Icon } from "@hugeicons/core-free-icons";
import { cn } from "@/lib/utils";
import { useDataTable } from "@/registry/default/data-table/data-table-context";
import {
InputGroup,
InputGroupAddon,
InputGroupInput,
} from "@/registry/default/input-group/input-group";
interface DataTableSearchProps extends Omit<
React.ComponentProps<"input">,
"value" | "onChange" | "size"
> {
placeholder?: string;
size?: "default" | "sm";
}
function DataTableSearch({
placeholder = "Search and filter",
size = "sm",
className,
...props
}: DataTableSearchProps) {
const { table } = useDataTable();
return (
<InputGroup className="flex-1 border-0 bg-transparent shadow-none outline-none! dark:bg-transparent">
<InputGroupAddon>
<HugeiconsIcon
icon={Search01Icon}
className="text-muted-foreground size-4"
strokeWidth={2}
/>
</InputGroupAddon>
<InputGroupInput
placeholder={placeholder}
size={size}
value={table.getState().globalFilter ?? ""}
onChange={(e) => table.setGlobalFilter(e.target.value)}
className={cn("py-0", className)}
render={<Toolbar.Input />}
{...props}
/>
</InputGroup>
);
}
export { DataTableSearch };