Chalk UI
Getting started
Customization
Dark Mode
CLI
Components
Accordion
Address Input
Alert
App Layout
Autocomplete
Avatar
Badge
Breadcrumbs
Button
Calendar
Card
Carousel
Chart (Area)
Chart (Bar)
Chart (Donut)
Chart (Line)
Checkbox
Checkbox Group
Collapsible
Combobox
Command
Currency Input
DataGrid
Date Picker
Date Range Picker
Disclosure
Drawer
Dropdown Menu
Form
Horizontal Draggable Scroll
Hover Card
Loading Spinner
Loading Overlay
Modal
Native Select
Navigation Menu
Number Input
Page Header
Pagination
Phone Input
Popover
Progress Bar
Radio Group
Scroll Area
Select
Separator
Simple Dropzone
Skeleton
Static Tabs
Stats
Switch
Table
Tabs
Text Input
Textarea
Timeline
Toaster
Tooltip
Vertical Menu

DataGrid

Fully-featured data table component. Built on top of Tanstack's React Table.

Source
Name
Price
Category
Availability
Date
Visible
Page
1 / 6

Installation

npx @rahimstack@latest add datagrid

Usage

Define the type

First, you need to define the type of the data that will be displayed.

Example

type Product = {
    id: string
    name: string
    image: string
    visible: boolean
    availability: "in_stock" | "out_of_stock"
    price: number
    category: string | null
}

Column definition

  • You can define your columns using the defineDataGridColumns helper method. This method takes a callback function that returns an array of type ColumnDef.
  • Learn more about TanStack Table column definition.
It is preferable to use `useMemo` to avoid excessive re-rendering and performance issues.

Example

import { defineDataGridColumns } from "@/components/ui/datagrid"
 
type Product = {
    // ...
}
 
function Demo() {
    const columns = React.useMemo(() => defineDataGridColumns<Product>(() => [
        {
            accessorKey: "name",
            header: "Name",
        },
        {
            accessorKey: "price",
            header: "Price",
            cell: info => "$" + Intl.NumberFormat("en-US").format(info.getValue<number>()),
        },
        // ...
    ]), [])
 
    return <></>
}

Render the table

import { defineDataGridColumns, DataGrid } from "@/components/ui/datagrid"
import { fetchFakeData } from "./datagrid-fake-api"
import * as React from "react"
 
export type Product = {
    id: string
    name: string
    image: string
    visible: boolean
    availability: "in_stock" | "out_of_stock"
    price: number
    category: string | null
}
 
export function DataGridMinimalExample() {
 
    const [clientData, setClientData] = React.useState<Product[] | undefined>(undefined)
 
    React.useEffect(() => {
        (async function() {
            const res = await fetchFakeData()
            setClientData(res.rows)
        })()
    }, [])
 
    const columns = React.useMemo(() => defineDataGridColumns<Product>(() => [
        {
            accessorKey: "name",
            header: "Name",
            size: 60,
        },
        {
            accessorKey: "price",
            header: "Price",
            cell: info => "$" + Intl.NumberFormat("en-US").format(info.getValue<number>()),
            size: 40,
        },
        // ...
    ]), [])
 
    return (
        <DataGrid<Product>
            columns={columns}
            data={clientData}
            rowCount={clientData?.length ?? 0}
            isLoading={!clientData}
        />
    )
 
}

Loading state

  • isLoading: When data is coming in for the first time
  • isDataMutating: When data is already present

Initial state

You can pass an initial state to the DataGrid component. This is useful when you want to control the state of the DataGrid from outside.

<DataGrid<T>
    {...props}
    initialState={{
        globalFilter: "",
        sorting: [],
        pagination: { pageIndex: 0, pageSize: 5 },
        rowSelection: {},
        columnFilters: [],
        columnVisibility: {},
    }}
/>

Pagination

Pagination is handled on the client by default.

  • rowCount is used to calculate the page count based on the pageSize. This value should be the number of rows fetched.
    • If you are using server-side pagination, the value should reflect the total number of rows in the database based on the filters.

Server-side/Manual

  • You should update rowCount programmatically based on the data fetched.
  • Set enableManualPagination to true and track the current state manually.
import { PaginationState } from "@tanstack/react-table"
 
// ...
const [pagination, setPagination] = React.useState<PaginationState>({ pageIndex: 0, pageSize: 5 })
 
// ...
 
const { data, isLoading } = useFakeQuery({
    queryKey: ["products", pagination],
    queryFn: async () => {
        const fetchURL = new URL(/*...*/)
        fetchURL.searchParams.set("pageIndex", String(pagination.pageIndex))
        fetchURL.searchParams.set("limit", String(pagination.pageSize))
        const res = await fetch(fetchURL.href)
        return (await res.json())
    },
})
 
return (
    <DataGrid<T>
        columns={columns}
        data={data}
        rowCount={data?.length ?? 0}
        isLoading={isLoading}
        enableManualPagination={true}
        initialState={{ pagination }}
        onPaginationChange={setPagination}
    />
)

Sorting

Sorting is automatically handled on a column-to-column basis.

Disable sorting

Column-level

const columns = useMemo(() => defineDataGridColumns<Product>(() => [
    //...
    {
        id: "_actions",
        enableSorting: false,
        enableGlobalFilter: false,
        cell: ({ row }) => <div className={"flex justify-end w-full"}>{/*...*/}</div>
    },
    //...
]), [])

Globally

<DataGrid<Product>
    {...props}
    enableSorting={false}
/>

Server-side/Manual

import { SortingState } from "@tanstack/react-table"
 
// ...
const [sorting, setSorting] = useState<SortingState>([])
 
// ...
 
const { data, isLoading } = useFakeQuery({
    queryKey: ["products", sorting],
    queryFn: async () => {
        const fetchURL = new URL(/*...*/)
        // Handle sending the sorting state to the server
        const res = await fetch(fetchURL.href)
        return (await res.json())
    },
})
 
return (
    <DataGrid<T>
        {...props}
        initialState={{ sorting }}
        onSortingChange={setSorting}
    />
)

Row selection

<DataGrid<T>
    {...props}
    enableRowSelection={true}
    onRowSelect={data => {
        console.log(data)
    }}
/>

Server-side/Manual

By default, row selection only works on rows that are fetched.

  • Set enablePersistentRowSelection to true to enable row selection on all rows.
  • You will have to provide a rowSelectionPrimaryKey in order to uniquely identify the rows.
<DataGrid<T>
    {...props}
    enableManualPagination={true}
    enableRowSelection={true}
    enablePersistentRowSelection={true}
    rowSelectionPrimaryKey="id" // or any other unique key
    onRowSelect={data => {
        console.log(data)
    }}
/>

Filtering

By default, filtering is handled on the client, so only the rows that are fetched will be filtered. Enable filtering on specific columns using the withFiltering and getFilterFn helpers.

Filter types

These are the pre-built filters

  • select
  • radio
  • checkbox
  • boolean
  • date-range
  • number 🏗️

You will have to provide both a filterFn prop for each column you want to enable filtering on by using the getFilterFn helper, and a meta prop with the filter configuration using the withFiltering helper.

const columns = useMemo(() => defineDataGridColumns<Product>(({ withValueFormatter, withFiltering, getFilterFn }) => [
    //...
    {
        accessorKey: "category",
        header: "Category",
        cell: info => info.getValue(),
        size: 20,
        filterFn: getFilterFn("radio"),
        meta: {
            ...withFiltering({
                name: "Category",
                type: "radio",
                options: [{ value: "Electronics" }, { value: "Food" }],
                icon: <BiFolder/>,
            }),
        },
    },
    {
        accessorKey: "availability",
        header: "Availability",
        cell: info => info.getValue(),
        size: 20,
        filterFn: getFilterFn("checkbox"),
        meta: {
            ...withValueFormatter<string>(value => { // Format the value for the filter buttons
                if (value === "out_of_stock") return "Out of stock"
                else if (value === "in_stock") return "In stock"
                return value
            }),
            ...withFiltering({
                name: "Availability",
                type: "checkbox",
                icon: <BiCheck/>,
                options: [
                    { value: "out_of_stock", label: "Out of stock" },
                    { value: "in_stock", label: "In stock" },
                ],
            }),
        },
    },
    {
        accessorKey: "visible",
        header: "Visible",
        cell: info => <Badge intent={info.getValue() === "Visible" ? "success" : "gray"}>{info.getValue<string>()}</Badge>,
        size: 20,
        filterFn: getFilterFn("boolean"),
        meta: {
            ...withValueFormatter<boolean, string>(value => value ? "Visible" : "Hidden"),
            ...withFiltering({
                name: "Visible",
                type: "boolean",
                icon: <BiLowVision/>,
                valueFormatter: (value) => { // Overrides `withValueFormatter`
                    return value ? "Yes" : "No"
                },
            }),
        },
    },
    ...
]), [])

Disable filtering

<DataGrid<Product>
    {...props}
    enableColumnFilters={false}
    enableGlobalFilter={false}
/>

Server-side/Manual

Filtering can be done server-side by setting enableManualFiltering to true.

The `rowCount` prop should reflect the total number of rows in the database based on the filters.
import { ColumnFiltersState } from "@tanstack/react-table"
 
// ...
const [globalFilter, setGlobalFilter] = React.useState<string>("")
const [columnFilters, setColumnFilters] = React.useState<ColumnFiltersState>([])
 
// ...
 
const { data, totalCount, isLoading } = useFakeQuery({
    queryKey: ["products", globalFilter, columnFilters],
    queryFn: async () => {
        const fetchURL = new URL(/*...*/)
        fetchURL.searchParams.set("nameLike", globalFilter)
        fetchURL.searchParams.set("category", columnFilters.find(f => f.id === "category")?.value as string ?? "")
        fetchURL.searchParams.set("visibility", String(columnFilters.find(f => f.id === "visibility")?.value ?? ""))
        // ...
        const res = await fetch(fetchURL.href)
        return (await res.json())
    },
})
 
return (
    <DataGrid<T>
        columns={columns}
        data={data}
        rowCount={totalCount}
        isLoading={isLoading}
        enableManualPagination={true}
        initialState={{ globalFilter, columnFilters }}
        onGlobalFilterChange={setGlobalFilter}
        onColumnFiltersChange={setColumnFilters}
    />
)

Column Visibility

You can hide certain columns below a specific pixel width. The width is based on the table element.

<DataGrid<Product>
    {...props}
    hideColumns={[
        { below: 850, hide: ["availability", "price"] },
        { below: 600, hide: ["_actions"] },
        { below: 515, hide: ["category"] },
        { below: 400, hide: ["visible"] },
    ]}
/>

Editing

Making a column editable

  • Only one row can be edited at a time. Users can edit multiple cells in a row simultaneously.
  • Use the onRowEdit callback function prop to get access to the updated row data.
const columns = useMemo(() => defineDataGridColumns<Product>(({ withEditing }) => [
    {
        accessorKey: "name",
        header: "Name",
        cell: info => info.getValue(),
        meta: {
            ...withEditing<string>({
                field: ({ onChange, ...ctx }) => (
                    <TextInput {...ctx} onTextChange={onChange} intent="unstyled" />
                ),
            }),
        },
    },
    // ...
]), [])
  • withEditing accepts a field prop that is used to render the input field.
  • field is a callback function that returns a React element. It has the following parameters:
    • context: An object containing a ref, value, and onChange function to control the input field.
    • options: An object containing the row errors rowErrors: DataGridValidationRowErrors, table, row, and cell instances.

You can specify the type of the field using the zodType prop on withEditing or by using generics.

const schema = defineSchema(({z}) => z.object({
    name: z.string(),
    ...
}))
 
// Column def
 
meta: {
    ...withEditing({
        zodType: schema.shape.name, // Define type from zod
        field: (ctx) => {
            console.log(ctx.value) // string
        },
    }),
}
 
meta: {
    ...withEditing<string>({ // Or define it from generics
        field: (ctx) => {
            console.log(ctx.value) // string
        },
    }),
}

Example

const columns = useMemo(() => defineDataGridColumns<Product>(({ withEditing }) => [
    {
        accessorKey: "name",
        header: "Name",
        cell: info => info.getValue(),
        meta: {
            ...withEditing<string>({
                field: ({ onChange, ...ctx }, { rowErrors, row }) => {
                    // Get field's error
                    const error = rowErrors.find(n => n.key === "name" && n.rowId === row.id)
                    
                    return <TextInput {...ctx} onTextChange={onChange} intent="unstyled" />
                },
            }),
        },
    },
    // ...
]), [])

Validation

  • You can pass a zod schema to the DataGrid component using the validationSchema prop.
  • By using the zod schema, you can validate the data before onRowEdit is called on the client or on the server.
  • You can also use the onRowValidationError callback function prop to get access to the validation errors.
const schema = defineSchema(({ z }) => z.object({
    name: z.string().min(3), // The name cell value will be validated against this schema
}))
 
const columns = useMemo(() => defineDataGridColumns<Product>(({ withEditing }) => [
    {
        accessorKey: "name",
        header: "Name",
        meta: {
            ...withEditing<string>({
                field: (ctx) => {/*...*/},
            }),
        }
    },
    ...rest
]), [])
 
return (
    <DataGrid<T>
        {...props}
        validationSchema={schema}
        onRowEdit={({ data, originalData, row }) => {
            // ...
        }}
        onRowValidationError={({ errors }) => {
            errors.forEach(error => {
                toast.error(error.path.join("."), {
                    description: error.message,
                })
            })
        }}
    />
)

Event

onRowEdit will be called when the user clicks the save button and the row data is valid.

  • data: The row data containing the updated values
  • originalData: The original row data
  • row: Row API. See reference.
<DataGrid<T>
    {...props}
    onRowEdit={({ data, originalData, row }) => {
        // ...
    }}
/>

Optimistic updates

  1. Set enableOptimisticUpdates to true
  2. Provide a optimisticUpdatePrimaryKey that will be used by the DataGrid to locate the row and perform the optimistic updates
  3. Handle potential errors by refetching/refreshing the data in case of an internal server error
  4. When optimistic updates are enabled, DataGrid will not display a loading state when isDataMutating is true

Note: Optimistic updates are paused when server-side validation is done using the zod schema.

Server-side validation

const { data, isLoading, refetch } = useFakeQuery({ ...queryOptions })
 
const { mutate, isMutating } = useFakeMutation({ ...mutationOptions })
 
const [isValidating, setIsValidating] = useState(false)
const serverSideNameValidation = React.useCallback(async (value: string) => {
    setIsValidating(true)
    const res = await verifyNameIsUnique(value) // Server-side validation
    setIsValidating(false)
    return res.isUnique
}, [])
 
const schema = defineSchema(({z}) => z.object({
    name: z.string().refine(value => serverSideNameValidation(value), { message: "Name already exists" }),
    // ...
}))
 
return (
    <DataGrid<Product>
        {...props}
        isLoading={isLoading}
        isDataMutating={isMutating || isValidating}
        validationSchema={schema}
        enableOptimisticUpdates={true}
        onRowEdit={({ data, originalData, row }) => {
            mutate(data, {
                onSuccess: () => refetch(),
                onError: () => refetch(),
            })
        }}
        onRowValidationError={({ errors }) => {
            // ...
        }}
    />
)

Server-side example

Name
Price
Category
Availability
Visible
Date

API Reference

const DataGridAnatomy = defineStyleAnatomy({
    root: cva([
        "UI-DataGrid__root",
    ]),
    header: cva([
        "UI-DataGrid__header",
        "block space-y-4 w-full mb-4",
    ]),
    toolbar: cva([
        "UI-DataGrid__toolbar",
        "flex w-full items-center gap-4 flex-wrap",
    ]),
    tableContainer: cva([
        "UI-DataGrid__tableContainer",
        "align-middle inline-block min-w-full max-w-full overflow-x-auto relative",
    ]),
    table: cva([
        "UI-DataGrid__table",
        "w-full relative table-fixed",
    ]),
    tableHead: cva([
        "UI-DataGrid__tableHead",
        "border-b",
    ]),
    th: cva([
        "UI-DataGrid__th group/th",
        "px-3 h-12 text-left text-sm font-bold",
        "data-[is-selection-col=true]:px-3 data-[is-selection-col=true]:sm:px-1 data-[is-selection-col=true]:text-center",
    ]),
    titleChevronContainer: cva([
        "UI-DataGrid__titleChevronContainer",
        "absolute flex items-center inset-y-0 top-1 -right-9 group",
    ]),
    titleChevron: cva([
        "UI-DataGrid__titleChevron",
        "mr-3 h-4 w-4 text-gray-400 group-hover:text-gray-500 relative bottom-0.5",
    ]),
    tableBody: cva([
        "UI-DataGrid__tableBody",
        "divide-y divide-[--border] w-full relative",
    ]),
    td: cva([
        "UI-DataGrid__td",
        "px-2 py-2 w-full whitespace-nowrap text-base font-normal text-[--foreground]",
        "data-[is-selection-col=true]:px-2 data-[is-selection-col=true]:sm:px-0 data-[is-selection-col=true]:text-center",
        "data-[action-col=false]:truncate data-[action-col=false]:overflow-ellipsis",
        "data-[row-selected=true]:bg-brand-50 dark:data-[row-selected=true]:bg-gray-800",
        "data-[editing=true]:ring-1 data-[editing=true]:ring-[--ring] ring-inset",
        "data-[editable=true]:hover:bg-[--subtle] md:data-[editable=true]:focus:ring-2 md:data-[editable=true]:focus:ring-[--slate]",
        "focus:outline-none",
    ]),
    tr: cva([
        "UI-DataGrid__tr",
        "hover:bg-[--subtle] truncate",
    ]),
    footer: cva([
        "UI-DataGrid__footer",
        "flex flex-col sm:flex-row w-full items-center gap-2 justify-between p-2 mt-2 overflow-x-auto max-w-full",
    ]),
    footerPageDisplayContainer: cva([
        "UI-DataGrid__footerPageDisplayContainer",
        "flex flex-none items-center gap-1 ml-2 text-sm",
    ]),
    footerPaginationInputContainer: cva([
        "UI-DataGrid__footerPaginationInputContainer",
        "flex flex-none items-center gap-2",
    ]),
    filterDropdownButton: cva([
        "UI-DataGrid__filterDropdownButton",
        "flex gap-2 items-center bg-[--paper] border rounded-[--radius] h-10 py-1 px-3 cursor-pointer hover:bg-[--subtle]",
        "select-none focus-visible:ring-2 outline-none ring-[--ring]",
    ]),
    editingCard: cva([
        "UI-DataGrid__editingCard",
        "flex items-center gap-2 rounded-md px-3 py-2",
    ]),
})