Docs
Table
Table
Displays data in rows and columns and enables a user to navigate its contents via directional navigation keys, and optionally supports row selection and sorting.
Loading...
Installation
"use client";
import { createContext, useContext, useMemo } from "react";
import { cva } from "cva";
import type {
ColumnProps,
TableBodyProps as RaTableBodyProps,
RowProps,
} from "react-aria-components";
import {
Cell as RaCell,
Column as RaColumn,
Row as RaRow,
Table as RaTable,
TableBody as RaTableBody,
TableHeader as RaTableHeader,
} from "react-aria-components";
import { autoRef, cn, withRenderProps } from "@/lib/utils";
/* -------------------------------------------------------------------------- */
/* Context */
/* -------------------------------------------------------------------------- */
interface TableContextValues {
bodySize?: "sm" | "default" | "lg";
headerSize?: "sm" | "default" | "lg";
stickyHeader?: boolean;
}
const TableContext = createContext<TableContextValues | null>(null);
function useTableContext() {
const context = useContext(TableContext);
if (!context) {
throw new Error("useTableContext must be used within a TableProvider");
}
return context;
}
/* -------------------------------------------------------------------------- */
/* Helpers */
/* -------------------------------------------------------------------------- */
interface CellProps {
cellWidth?: number;
align?: "left" | "center" | "right";
}
const getCellStyle = ({ cellWidth, align }: CellProps) => {
return {
textAlign: align,
width: cellWidth ? `${cellWidth}%` : undefined,
};
};
/* -------------------------------------------------------------------------- */
/* Variants */
/* -------------------------------------------------------------------------- */
export const tableVariants = {
wrapper: cva({
base: "w-full grow overflow-auto",
}),
root: cva({
base: "w-full caption-bottom text-sm",
}),
header: cva({
base: "[&_tr]:border-b",
}),
body: cva({
base: "[&_tr:last-child]:border-0 [&_tr_>_td:last-child]:s-empty:p-4 [&_tr_>_td:last-child]:s-empty:text-center s-empty:text-sm s-empty:text-muted-foreground",
}),
row: cva({
base: "border-b transition-colors s-focus:outline-none",
}),
column: cva({
base: "text-left align-middle font-medium text-muted-foreground [&>[role=checkbox]]:translate-y-[2px] s-focus:outline-none s-allows-sorting:cursor-pointer",
variants: {
stickyHeader: {
true: "sticky top-0 z-50 bg-white drop-shadow",
false: "",
},
},
defaultVariants: {
stickyHeader: false,
},
}),
cell: cva({
base: cn("px-4 align-middle [&:has([role=checkbox])]:pr-0"),
variants: {
size: {
sm: "h-12",
default: "h-14",
lg: "h-16",
},
},
defaultVariants: {
size: "default",
},
}),
};
/* -------------------------------------------------------------------------- */
/* Components */
/* -------------------------------------------------------------------------- */
/* ---------------------------------- Root ---------------------------------- */
export type TableProps = React.ComponentPropsWithRef<typeof RaTable> &
TableContextValues & {
ariaLabel: string;
classNames?: {
wrapper?: string;
root?: string;
};
};
export const Table = autoRef(
({
className,
classNames,
ariaLabel,
bodySize = "default",
stickyHeader,
headerSize = "default",
...props
}: TableProps) => {
const tableContextValues = useMemo(
() => ({ bodySize, stickyHeader, headerSize }),
[bodySize, stickyHeader, headerSize],
);
return (
<TableContext.Provider value={tableContextValues}>
<div
className={cn(
tableVariants.wrapper(),
className,
classNames?.wrapper,
)}
>
<RaTable
className={cn(tableVariants.root(), classNames?.root)}
aria-label={ariaLabel}
{...props}
/>
</div>
</TableContext.Provider>
);
},
);
/* ---------------------------------- Body ---------------------------------- */
export type TableBodyProps<TData extends object> = RaTableBodyProps<TData> & {
ref?: React.ComponentPropsWithRef<typeof RaTableBody>["ref"];
};
const TableBodyInteral = <TData extends object>({
className,
...props
}: TableBodyProps<TData>) => {
return (
<RaTableBody<TData>
className={cn(tableVariants.body(), className)}
{...props}
/>
);
};
export const TableBody = autoRef(TableBodyInteral);
/* ---------------------------------- Head ---------------------------------- */
export type TableHeadProps = React.ComponentPropsWithRef<typeof RaTableHeader>;
export const TableHead = autoRef(({ className, ...props }: TableHeadProps) => {
return (
<RaTableHeader
className={cn(tableVariants.header(), className)}
{...props}
/>
);
});
/* --------------------------------- Column --------------------------------- */
export interface TableColumnProps<TData extends object>
extends ColumnProps<TData>,
CellProps {
ref?: React.ComponentPropsWithRef<typeof RaColumn>["ref"];
}
const TableColumnInternal = <TData extends object>({
className,
cellWidth,
align,
...props
}: TableColumnProps<TData>) => {
const { stickyHeader, headerSize } = useTableContext();
return (
<RaColumn
className={cn(
tableVariants.cell({ size: headerSize }),
tableVariants.column({ stickyHeader }),
className,
)}
style={{
...(stickyHeader && {
clipPath: "polygon(0 0, 100% 0, 100% 130%, 0 130%)",
}),
...getCellStyle({ align, cellWidth }),
}}
{...props}
/>
);
};
export const TableColumn = autoRef(TableColumnInternal);
/* ---------------------------------- Cell ---------------------------------- */
export type TableCellProps = React.ComponentPropsWithRef<typeof RaCell> &
CellProps;
export const TableCell = autoRef(
({ className, cellWidth, align, ...props }: TableCellProps) => {
const { bodySize } = useTableContext();
return (
<RaCell
className={(values) =>
cn(
tableVariants.cell({ size: bodySize }),
withRenderProps(className)(values),
)
}
style={getCellStyle({ cellWidth, align })}
{...props}
/>
);
},
);
/* ----------------------------------- Row ---------------------------------- */
export interface TableRowProps<TData extends object> extends RowProps<TData> {
ref?: React.ComponentPropsWithRef<typeof RaRow>["ref"];
}
const TableRowInternal = <TData extends object>({
className,
...props
}: TableRowProps<TData>) => {
return <RaRow className={cn(tableVariants.row(), className)} {...props} />;
};
export const TableRow = autoRef(TableRowInternal);
/* ------------------------------- CellActions ------------------------------ */
export type TableCellActionsProps = React.ComponentPropsWithRef<"div">;
export const TableCellActions = ({
children,
className,
}: {
children?: React.ReactNode;
className?: React.ReactNode;
}) => {
return <div className={cn("flex justify-end", className)}>{children}</div>;
};