---
title: Data Table
description: Powerful table and datagrids built using TanStack Table.
link:
  doc: https://tanstack.com/table/latest/docs/introduction
---

<ComponentPreview name="data-table-demo">

```tsx file=<rootDir>/apps/docs/src/registry/examples/data-table-demo.tsx

```

</ComponentPreview>

## Introduction

Every data table or datagrid I've created has been unique. They all behave differently, have specific sorting and filtering requirements, and work with different data sources.

It doesn't make sense to combine all of these variations into a single component. If we do that, we'll lose the flexibility that [headless UI](https://tanstack.com/table/v8/docs/guide/introduction#what-is-headless-ui) provides.

So instead of a data-table component, I thought it would be more helpful to provide a guide on how to build your own.

We'll start with the basic `<Table />` component and build a complex data table from scratch.

**Tip:** If you find yourself using the same table in multiple places in your app, you can always extract it into a reusable component.

## Table of Contents

This guide will show you how to use [TanStack Table](https://tanstack.com/table) and the `<Table />` component to build your own custom data table. We'll cover the following topics:

- [Basic Table](#basic-table)
- [Row Actions](#row-actions)
- [Pagination](#pagination)
- [Sorting](#sorting)
- [Filtering](#filtering)
- [Visibility](#visibility)
- [Row Selection](#row-selection)

## Installation

1. Add the `<Table />` component to your project:

```bash
npx shadcn-solid@latest add table button dropdown-menu textfield checkbox
```

2. Add `tanstack/solid-table` dependency:

```bash
npm install @tanstack/solid-table
```

## Prerequisites

We are going to build a table to show tasks. Here's what our data looks like:

```tsx
type Task = {
  id: string
  code: string
  title: string
  status: "todo" | "in-progress" | "done" | "cancelled"
  label: "bug" | "feature" | "enhancement" | "documentation"
}

export const tasks: Task[] = [
  {
    id: "ptL0KpX_yRMI98JFr6B3n",
    code: "TASK-33",
    title: "We need to bypass the redundant AI interface!",
    status: "todo",
    label: "bug",
  },
  {
    id: "RsrTg_SmBKPKwbUlr7Ztv",
    code: "TASK-59",
    title:
      "Overriding the capacitor won't do anything, we need to generate the solid state JBOD pixel!",
    status: "in-progress",
    label: "feature",
  },
  // ...
]
```

## Project Structure

Start by creating the following file structure:

```txt
src
└── routes
    ├── _components
    │   ├── columns.tsx
    │   └── data-table.tsx
    └── index.tsx
```

- `columns.tsx` will contain our column definitions.
- `data-table.tsx` will contain our `<DataTable />` component.
- `index.tsx` is where we'll fetch data and render our table.

## Basic Table

Let's start by building a basic table.

<Step>

### Column Definitions

First, we'll define our columns.

```tsx title="src/routes/_components/columns.tsx" {1,13-26}
import type { ColumnDef } from "@tanstack/solid-table"

// This type is used to define the shape of our data.
// You can use a Zod or Validbot schema here if you want.
export type Task = {
  id: string
  code: string
  title: string
  status: "todo" | "in-progress" | "done" | "cancelled"
  label: "bug" | "feature" | "enhancement" | "documentation"
}

export const columns: ColumnDef<Task>[] = [
  {
    accessorKey: "code",
    header: "Task",
  },
  {
    accessorKey: "title",
    header: "Title",
  },
  {
    accessorKey: "status",
    header: "Status",
  },
]
```

### `<DataTable />` component

Next, we'll create a `<DataTable />` component to render our table.

```tsx title="src/routes/_components/data-table.tsx"
import { Accessor, For, Show, splitProps } from "solid-js"
import {
  createSolidTable,
  flexRender,
  getCoreRowModel,
  type ColumnDef,
} from "@tanstack/solid-table"

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

type Props<TData, TValue> = {
  columns: ColumnDef<TData, TValue>[]
  data: Accessor<TData[] | undefined>
}

export const DataTable = <TData, TValue>(props: Props<TData, TValue>) => {
  const [local] = splitProps(props, ["columns", "data"])

  const table = createSolidTable({
    get data() {
      return local.data() || []
    },
    columns: local.columns,
    getCoreRowModel: getCoreRowModel(),
  })

  return (
    <div class="rounded-md border">
      <Table>
        <TableHeader>
          <For each={table.getHeaderGroups()}>
            {(headerGroup) => (
              <TableRow>
                <For each={headerGroup.headers}>
                  {(header) => {
                    return (
                      <TableHead>
                        {header.isPlaceholder
                          ? null
                          : flexRender(
                              header.column.columnDef.header,
                              header.getContext(),
                            )}
                      </TableHead>
                    )
                  }}
                </For>
              </TableRow>
            )}
          </For>
        </TableHeader>
        <TableBody>
          <Show
            when={table.getRowModel().rows?.length}
            fallback={
              <TableRow>
                <TableCell
                  colSpan={local.columns.length}
                  class="h-24 text-center"
                >
                  No results.
                </TableCell>
              </TableRow>
            }
          >
            <For each={table.getRowModel().rows}>
              {(row) => (
                <TableRow data-state={row.getIsSelected() && "selected"}>
                  <For each={row.getVisibleCells()}>
                    {(cell) => (
                      <TableCell>
                        {flexRender(
                          cell.column.columnDef.cell,
                          cell.getContext(),
                        )}
                      </TableCell>
                    )}
                  </For>
                </TableRow>
              )}
            </For>
          </Show>
        </TableBody>
      </Table>
    </div>
  )
}
```

### Render the table

Finally, we'll render our table in our index page.

```tsx title="src/routes/index.tsx" {29}
import { cache, createAsync, type RouteDefinition } from "@solidjs/router"

import { columns, type Task } from "./_components/columns"
import { DataTable } from "./_components/data-table"

const getData = cache(async (): Promise<Task[]> => {
  // Fetch data from your API here.
  return [
    {
      id: "ptL0KpX_yRMI98JFr6B3n",
      code: "TASK-33",
      title: "We need to bypass the redundant AI interface!",
      status: "todo",
      label: "bug",
    },
    // ...
  ]
}, "data")

export const route: RouteDefinition = {
  load: () => getData(),
}

const Home = () => {
  const data = createAsync(() => getData())

  return (
    <div class="w-full space-y-2.5">
      <DataTable columns={columns} data={data} />
    </div>
  )
}

export default Home
```

</Step>

## Row Actions

Update our columns definition to add a new `actions` column. The `actions` cell returns a `<Dropdown />` component.

```tsx title="src/routes/_components/columns.tsx" {2-7,11-37}
//...
import {
  DropdownMenu,
  DropdownMenuContent,
  DropdownMenuItem,
  DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu"

export const columns: ColumnDef<Task>[] = [
  // ...
  {
    id: "actions",
    cell: () => (
      <DropdownMenu placement="bottom-end">
        <DropdownMenuTrigger class="flex items-center justify-center">
          <svg
            xmlns="http://www.w3.org/2000/svg"
            class="size-4"
            viewBox="0 0 24 24"
          >
            <path
              fill="none"
              stroke="currentColor"
              stroke-linecap="round"
              stroke-linejoin="round"
              stroke-width="2"
              d="M4 12a1 1 0 1 0 2 0a1 1 0 1 0-2 0m7 0a1 1 0 1 0 2 0a1 1 0 1 0-2 0m7 0a1 1 0 1 0 2 0a1 1 0 1 0-2 0"
            />
          </svg>
        </DropdownMenuTrigger>
        <DropdownMenuContent>
          <DropdownMenuItem>Edit</DropdownMenuItem>
          <DropdownMenuItem>Delete</DropdownMenuItem>
        </DropdownMenuContent>
      </DropdownMenu>
    ),
  },
  // ...
]
```

You can access the row data using `row.original` in the `cell` function. Use this to handle actions for your row eg. use the `id` to make a DELETE call to your API.

## Pagination

Next, we'll add pagination to our table.

<Step>

### Update `<DataTable />`

```tsx title="src/routes/_components/data-table.tsx" {4,10}
//...
import {
  //...
  getPaginationRowModel,
} from "@tanstack/solid-table"

export const DataTable = <TData, TValue>(props: Props<TData, TValue>) => {
  const table = createSolidTable({
    //...
    getPaginationRowModel: getPaginationRowModel(),
  })

  // ...
}
```

This will automatically paginate your rows into pages of 10. See the [pagination docs](https://tanstack.com/table/v8/docs/api/features/pagination) for more information on customizing page size and implementing manual pagination.

### Add pagination controls

We can add pagination controls to our table using the `<Button />` component and the `table.previousPage()`, `table.nextPage()` API methods.

```tsx title="src/routes/_components/data-table.tsx" {2,11-28}
//...
import { Button } from "@/components/ui/button"

export const DataTable = <TData, TValue>(props: Props<TData, TValue>) {
  //...

  return (
    <div>
      <div class="rounded-md border">...
      </div>
      <div class="flex items-center justify-end space-x-2 py-4">
        <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>
  )
}
```

</Step>

## Sorting

<Step>

### Update `<DataTable />`

```tsx title="src/routes/_components/data-table.tsx" {4,8,12-18}
//...
import {
  //...
  getSortedRowModel,
} from "@tanstack/solid-table"

export const DataTable = <TData, TValue>(props: Props<TData, TValue>) => {
  const [sorting, setSorting] = createSignal<SortingState>([])

  const table = createSolidTable({
    //...
    onSortingChange: setSorting,
    getSortedRowModel: getSortedRowModel(),
    state: {
      get sorting() {
        return sorting()
      },
    },
  })

  // ...
}
```

### Make header cell sortable

Update the `status` header cell to add sorting controls.

```tsx title="src/routes/_components/columns.tsx" {2,6-33}
//...
import { Button } from "@/components/ui/button"

export const columns: ColumnDef<Task>[] = [
  // ...
  {
    accessorKey: "status",
    header: ({ column }) => {
      return (
        <Button
          variant="ghost"
          onClick={() => column.toggleSorting(column.getIsSorted() === "asc")}
        >
          Status
          <svg
            xmlns="http://www.w3.org/2000/svg"
            class="ml-2 size-4"
            aria-hidden="true"
            viewBox="0 0 24 24"
          >
            <path
              fill="none"
              stroke="currentColor"
              stroke-linecap="round"
              stroke-linejoin="round"
              stroke-width="2"
              d="M12 5v14m4-4l-4 4m-4-4l4 4"
            />
          </svg>
        </Button>
      )
    },
  },
  // ...
]
```

</Step>

## Filtering

Add filter to the title. See the [filtering docs](https://tanstack.com/table/v8/docs/guide/filters) for more information on customizing filters.

### Update `<DataTable />`

```tsx title="src/routes/_components/data-table.tsx" {4-5,11,15-16,19-21,27-40}
//...
import {
  //...
  ColumnFiltersState,
  getFilteredRowModel,
} from "@tanstack/solid-table"
import { TextField, TextFieldRoot } from "~/components/ui/textfield"

export const DataTable = <TData, TValue>(props: Props<TData, TValue>) => {
  //...
  const [columnFilters, setColumnFilters] = createSignal<ColumnFiltersState>([])

  const table = createSolidTable({
    //...
    getFilteredRowModel: getFilteredRowModel(),
    onColumnFiltersChange: setColumnFilters,
    state: {
      //...
      get columnFilters() {
        return columnFilters()
      },
    },
  })

  return (
    <div>
      <div class="flex items-center py-4">
        <TextFieldRoot>
          <TextField
            placeholder="Filter title..."
            value={(table.getColumn("title")?.getFilterValue() as string) ?? ""}
            onInput={(event) =>
              table
                .getColumn("title")
                ?.setFilterValue(event.currentTarget.value)
            }
            class="max-w-sm"
          />
        </TextFieldRoot>
      </div>
      <div class="rounded-md border">...</div>
    </div>
  )
}
```

## Visibility

Adding column visibility using visibility API.

### Update `<DataTable />`

```tsx title="src/routes/_components/data-table.tsx" {4,19-21,25,28-30,38-63}
//...
import {
  //...
  VisibilityState,
} from "@tanstack/solid-table"
import { As } from "@kobalte/core"
import type { DropdownMenuTriggerProps } from "@kobalte/core/dropdown-menu"
import { Button } from "~/components/ui/button"

import {
  DropdownMenu,
  DropdownMenuCheckboxItem,
  DropdownMenuContent,
  DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu"

export const DataTable = <TData, TValue>(props: Props<TData, TValue>) => {
  //...
  const [columnVisibility, setColumnVisibility] = createSignal<VisibilityState>(
    {},
  )

  const table = createSolidTable({
    //...
    onColumnVisibilityChange: setColumnVisibility,
    state: {
      //...
      get columnVisibility() {
        return columnVisibility()
      },
    },
  })

  return (
    <div>
      <div class="flex items-center py-4">
        //...
        <DropdownMenu>
          <DropdownMenuTrigger
            as={(props: DropdownMenuTriggerProps) => (
              <Button {...props} variant="outline" class="ml-auto">
                Columns
              </Button>
            )}
          />
          <DropdownMenuContent>
            <For
              each={table
                .getAllColumns()
                .filter((column) => column.getCanHide())}
            >
              {(item) => (
                <DropdownMenuCheckboxItem
                  class="capitalize"
                  checked={item.getIsVisible()}
                  onChange={(value) => item.toggleVisibility(!!value)}
                >
                  {item.id}
                </DropdownMenuCheckboxItem>
              )}
            </For>
          </DropdownMenuContent>
        </DropdownMenu>
      </div>
      <div class="rounded-md border">...</div>
    </div>
  )
}
```

## Row Selection

Next, we're going to add row selection to our table.

<Step>

### Update column definitions

```tsx title="src/routes/_components/columns.tsx" {2,5-28}
//...
import { Checkbox, CheckboxControl } from "@/components/ui/checkbox"

export const columns: ColumnDef<Task>[] = [
  {
    id: "select",
    header: ({ table }) => (
      <Checkbox
        indeterminate={table.getIsSomePageRowsSelected()}
        checked={table.getIsAllPageRowsSelected()}
        onChange={(value) => table.toggleAllPageRowsSelected(!!value)}
        aria-label="Select all"
      >
        <CheckboxControl />
      </Checkbox>
    ),
    cell: ({ row }) => (
      <Checkbox
        checked={row.getIsSelected()}
        onChange={(value) => row.toggleSelected(!!value)}
        aria-label="Select row"
      >
        <CheckboxControl />
      </Checkbox>
    ),
    enableSorting: false,
    enableHiding: false,
  },
  // ...
]
```

### Update `<DataTable />`

```tsx title="src/routes/_components/data-table.tsx" {5,9,12-14}
//...

export const DataTable = <TData, TValue>(props: Props<TData, TValue>) => {
  //...
  const [rowSelection, setRowSelection] = createSignal({})

  const table = createSolidTable({
    //...
    onRowSelectionChange: setRowSelection,
    state: {
      // ...
      get rowSelection() {
        return rowSelection()
      },
    },
  })

  return <div>...</div>
}
```

### Show selected rows

You can show the number of selected rows using the `table.getFilteredSelectedRowModel()` API.

```tsx
<div class="text-muted-foreground flex-1 text-sm">
  {table.getFilteredSelectedRowModel().rows.length} of{" "}
  {table.getFilteredRowModel().rows.length} row(s) selected.
</div>
```

</Step>
