GitHub

Data Table

Powerful table and datagrids built using TanStack Table.

Docs
Status
Amount
success
ken99@yahoo.com
$316.00
success
Abe45@gmail.com
$242.00
processing
Monserrat44@gmail.com
$837.00
success
Silas22@gmail.com
$874.00
failed
carmella@hotmail.com
$721.00
0 of 5 row(s) selected.

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 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 and the <Table /> component to build your own custom data table. We'll cover the following topics:

Installation

  1. Add the <Table /> component to your project:
npx solidui-cli@latest add table
  1. Add tanstack/solid-table dependency:
npm install @tanstack/solid-table

Prerequisites

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

type Payment = {
  id: string
  amount: number
  status: "pending" | "processing" | "success" | "failed"
  email: string
}
 
export const payments: Payment[] = [
  {
    id: "728ed52f",
    amount: 100,
    status: "pending",
    email: "m@example.com"
  },
  {
    id: "489e1d42",
    amount: 125,
    status: "processing",
    email: "example@gmail.com"
  }
  // ...
]

Project Structure

Start by creating the following file structure:

src
└── routes
    └── payments
        ├── 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.

Column Definitions

First, we'll define our columns.

columns.tsx
import { ColumnDef } from "@tanstack/solid-table"
 
export type Payment = {
  id: string
  amount: number
  status: "pending" | "processing" | "success" | "failed"
  email: string
}
 
export const columns: ColumnDef<Payment>[] = [
  {
    accessorKey: "status",
    header: "Status"
  },
  {
    accessorKey: "email",
    header: "Email"
  },
  {
    accessorKey: "amount",
    header: "Amount"
  }
]

Note: Columns are where you define the core of what your table will look like. They define the data that will be displayed, how it will be formatted, sorted and filtered.

<DataTable /> component

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

data-table.tsx
import { For, Show } from "solid-js"
import { ColumnDef, createSolidTable, flexRender, getCoreRowModel } from "@tanstack/solid-table"
 
import {
  Table,
  TableBody,
  TableCell,
  TableHead,
  TableHeader,
  TableRow
} from "~/components/ui/table"
 
interface DataTableProps<TData, TValue> {
  columns: ColumnDef<TData, TValue>[]
  data: TData[]
}
 
export function DataTable<TData, TValue>(props: DataTableProps<TData, TValue>) {
  const table = createSolidTable({
    get data() {
      return props.data
    },
    get columns() {
      return props.columns
    }
    getCoreRowModel: getCoreRowModel()
  })
 
  return (
    <div class="rounded-md border">
      <Table>
        <TableHeader>
          <For each={table.getHeaderGroups()}>
            {(headerGroup) => (
              <TableRow>
                <For each={headerGroup.headers}>
                  {(header) => (
                      <TableHead colSpan={header.colSpan}>
                        <Show when={!header.isPlaceholder}>
                          {flexRender(header.column.columnDef.header, header.getContext())}
                        </Show>
                      </TableHead>
                    )}
                </For>
              </TableRow>
            )}
          </For>
        </TableHeader>
        <TableBody>
            <Show
              when={table.getRowModel().rows?.length}
              fallback={
                <TableRow>
                  <TableCell colSpan={props.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>
  )
}

Tip: If you find yourself using <DataTable /> in multiple places, this is the component you could make reusable by extracting it to components/ui/data-table.tsx.

<DataTable columns={columns} data={data} />

Render the table

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

index.tsx
import { createAsync, query } from "@solidjs/router"
 
import { columns, Payment } from "./columns"
import { DataTable } from "./data-table"
 
const getData = query(async () => {
  "use server"
  // Fetch data from your api
  return [
    {
      id: "728ed52f",
      amount: 100,
      status: "pending",
      email: "m@example.com"
    }
    // ...
  ]
}, "getData")
 
export default async function DemoPage() {
  const data = createAsync(() => getData())
 
  return (
    <div class="container mx-auto py-10">
      <DataTable columns={columns} data={data()} />
    </div>
  )
}

Cell Formatting

Let's format the amount cell to display the dollar amount. We'll also align the cell to the right.

Update columns definition

Update the header and cell definitions for amount as follows:

columns.tsx
export const columns: ColumnDef<Payment>[] = [
  {
    accessorKey: "amount",
    header: () => <div class="text-right">Amount</div>,
    cell: (props) => {
      const formatted = createMemo(() => {
        const amount = parseFloat(props.row.getValue("amount"))
        return new Intl.NumberFormat("en-US", {
          style: "currency",
          currency: "USD"
        }).format(amount)
      })
      return <div class="text-right font-medium">{formatted()}</div>
    }
  }
]

You can use the same approach to format other cells and headers.

Row Actions

Let's add row actions to our table. We'll use a <Dropdown /> component for this.

Update columns definition

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

columns.tsx
import { ColumnDef } from "@tanstack/solid-table"
import { MoreHorizontal } from "lucide-solid"
 
import { Button } from "~/components/ui/button"
import {
  DropdownMenu,
  DropdownMenuContent,
  DropdownMenuItem,
  DropdownMenuLabel,
  DropdownMenuSeparator,
  DropdownMenuTrigger
} from "~/components/ui/dropdown-menu"
 
export const columns: ColumnDef<Payment>[] = [
  // ...
  {
    id: "actions",
    enableHiding: false,
    cell: (props) => {
      return (
        <DropdownMenu placement="bottom-end">
          <DropdownMenuTrigger as={Button<"button">} variant="ghost" class="size-8 p-0">
            <span class="sr-only">Open menu</span>
            <IconDots />
          </DropdownMenuTrigger>
          <DropdownMenuContent>
            <DropdownMenuLabel>Actions</DropdownMenuLabel>
            <DropdownMenuItem onClick={() => navigator.clipboard.writeText(props.row.original.id)}>
              Copy payment ID
            </DropdownMenuItem>
            <DropdownMenuSeparator />
            <DropdownMenuItem>View customer</DropdownMenuItem>
            <DropdownMenuItem>View payment details</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.

Update <DataTable>

data-table.tsx
import {
  ColumnDef,
  createSolidTable,
  flexRender,
  getCoreRowModel,
  getPaginationRowModel
} from "@tanstack/solid-table"
 
export function DataTable<TData, TValue>(props: DataTableProps<TData, TValue>) {
  const table = createSolidTable({
    get data() {
      return props.data
    },
    get columns() {
      return props.columns
    },
    getCoreRowModel: getCoreRowModel(),
    getPaginationRowModel: getPaginationRowModel()
  })
 
  // ...
}

This will automatically paginate your rows into pages of 10. See the pagination docs 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.

data-table.tsx
import { Button } from "~/components/ui/button"
 
export function DataTable<TData, TValue>(props: DataTableProps<TData, TValue>) {
  const table = createSolidTable({
    get data() {
      return props.data
    },
    get columns() {
      return props.columns
    },
    getCoreRowModel: getCoreRowModel(),
    getPaginationRowModel: getPaginationRowModel(),
  })
 
  return (
    <div>
      <div class="rounded-md border">
        <Table>
          { // .... }
        </Table>
      </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>
  )
}

See Reusable Components section for a more advanced pagination component.

Sorting

Let's make the email column sortable.

Update <DataTable>

data-table.tsx
import { createSignal } from "solid-js"
 
import {
  ColumnDef,
  SortingState,
  flexRender,
  getCoreRowModel,
  getPaginationRowModel,
  getSortedRowModel,
  useReactTable,
} from "@tanstack/solid-table"
 
export function DataTable<TData, TValue>(props: DataTableProps<TData, TValue>) {
  const [sorting, setSorting] = createSignal<SortingState>([])
 
  const table = createSolidTable({
    get data() {
      return props.data
    },
    get columns() {
      return props.columns
    },
    getCoreRowModel: getCoreRowModel(),
    getPaginationRowModel: getPaginationRowModel(),
    onSortingChange: setSorting,
    getSortedRowModel: getSortedRowModel(),
    state: {
      get sorting() {
        return sorting()
      }
    },
  })
 
  return (
    <div>
      <div class="rounded-md border">
        <Table>{ ... }</Table>
      </div>
    </div>
  )
}

Make header cell sortable

We can now update the email header cell to add sorting controls.

columns.tsx
import { ColumnDef } from "@tanstack/react-table"
import { ArrowUpDown } from "lucide-solid"
 
export const columns: ColumnDef<Payment>[] = [
  {
    accessorKey: "email",
    header: (props) => {
      return (
        <Button
          variant="ghost"
          onClick={() => props.column.toggleSorting(props.column.getIsSorted() === "asc")}
        >
          Email
          <ArrowUpDown class="ml-2 h-4 w-4" />
        </Button>
      )
    }
  }
]

This will automatically sort the table (asc and desc) when the user toggles on the header cell.

Filtering

Let's add a search input to filter emails in our table.

Update <DataTable>

data-table.tsx
import { createSignal } from "solid-js"
 
import {
  ColumnDef,
  ColumnFiltersState,
  SortingState,
  flexRender,
  getCoreRowModel,
  getFilteredRowModel,
  getPaginationRowModel,
  getSortedRowModel,
  createSolidTable
} from "@tanstack/solid-table"
 
import { Button } from "~/components/ui/button"
import { TextField, TextFieldInput } from "~/components/ui/text-field"
 
export function DataTable<TData, TValue>(props: DataTableProps<TData, TValue>) {
  const [sorting, setSorting] = createSignal<SortingState>([])
  const [columnFilters, setColumnFilters] = createSignal<ColumnFiltersState>([])
 
  const table = createSolidTable({
    get data() {
      return props.data
    },
    get columns() {
      return props.columns
    },
    onSortingChange: setSorting,
    getCoreRowModel: getCoreRowModel(),
    getPaginationRowModel: getPaginationRowModel(),
    getSortedRowModel: getSortedRowModel(),
    onColumnFiltersChange: setColumnFilters,
    getFilteredRowModel: getFilteredRowModel(),
    state: {
      get sorting() {
        return sorting()
      },
      get columnFilters() {
        return columnFilters()
      }
    },
  })
 
  return (
    <div>
      <div class="flex items-center py-4">
        <TextField
          value={(table.getColumn("email")?.getFilterValue() as string) ?? ""}
          onChange={(value) => table.getColumn("email")?.setFilterValue(value)}
        >
          <TextFieldInput placeholder="Filter emails..." class="max-w-sm" />
        </TextField>
      </div>
      <div class="rounded-md border">
        <Table>{ ... }</Table>
      </div>
    </div>
  )
}

Filtering is now enabled for the email column. You can add filters to other columns as well. See the filtering docs for more information on customizing filters.

Visibility

Adding column visibility is fairly simple using @tanstack/solid-table visibility API.

Update <DataTable>

data-table.tsx
import { createSignal } from "solid-js"
 
import {
  ColumnDef,
  ColumnFiltersState,
  SortingState,
  VisibilityState,
  flexRender,
  getCoreRowModel,
  getFilteredRowModel,
  getPaginationRowModel,
  getSortedRowModel,
  createSolidTable,
} from "@tanstack/solid-table"
 
import { Button } from "~/components/ui/button"
import {
  DropdownMenu,
  DropdownMenuCheckboxItem,
  DropdownMenuContent,
  DropdownMenuTrigger,
} from "~/components/ui/dropdown-menu"
 
export function DataTable<TData, TValue>(props: DataTableProps<TData, TValue>) {
  const [sorting, setSorting] = createSignal<SortingState>([])
  const [columnFilters, setColumnFilters] = createSignal<ColumnFiltersState>([])
  const [columnVisibility, setColumnVisibility] = createSignal<VisibilityState>({})
 
  const table = createSolidTable({
    get data() {
      return props.data
    },
    get columns() {
      return props.columns
    },
    onSortingChange: setSorting,
    onColumnFiltersChange: setColumnFilters,
    getCoreRowModel: getCoreRowModel(),
    getPaginationRowModel: getPaginationRowModel(),
    getSortedRowModel: getSortedRowModel(),
    getFilteredRowModel: getFilteredRowModel(),
    onColumnVisibilityChange: setColumnVisibility,
    state: {
      get sorting() {
        return sorting()
      },
      get columnFilters() {
        return columnFilters()
      },
      get columnVisibility() {
        return columnVisibility()
      }
    },
  })
 
  return (
    <div>
      <div class="flex items-center py-4">
        <TextField
          value={(table.getColumn("email")?.getFilterValue() as string) ?? ""}
          onChange={(value) => table.getColumn("email")?.setFilterValue(value)}
        >
          <TextFieldInput placeholder="Filter emails..." class="max-w-sm" />
        </TextField>
        <DropdownMenu placement="bottom-end">
          <DropdownMenuTrigger as={Button<"button">} variant="outline" class="ml-auto">
            Columns <IconChevronDown />
          </DropdownMenuTrigger>
          <DropdownMenuContent>
            <For each={table.getAllColumns().filter((column) => column.getCanHide())}>
              {(column) => {
                return (
                  <DropdownMenuCheckboxItem
                    class="capitalize"
                    checked={column.getIsVisible()}
                    onChange={(value) => column.toggleVisibility(!!value)}
                  >
                    {column.id}
                  </DropdownMenuCheckboxItem>
                )
              }}
            </For>
          </DropdownMenuContent>
        </DropdownMenu>
      </div>
      <div class="rounded-md border">
        <Table>{ ... }</Table>
      </div>
    </div>
  )
}

This adds a dropdown menu that you can use to toggle column visibility.

Row Selection

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

Update column definitions

columns.tsx
import { ColumnDef } from "@tanstack/solid-table"
 
import { Badge } from "~/components/ui/badge"
import { Checkbox } from "~/components/ui/checkbox"
 
export const columns: ColumnDef<Payment>[] = [
  {
    id: "select",
    header: (props) => (
      <Checkbox
        checked={props.table.getIsAllPageRowsSelected()}
        indeterminate={props.table.getIsSomePageRowsSelected()}
        onChange={(value) => props.table.toggleAllPageRowsSelected(!!value)}
        aria-label="Select all"
      />
    ),
    cell: (props) => (
      <Checkbox
        checked={props.row.getIsSelected()}
        onChange={(value) => props.row.toggleSelected(!!value)}
        aria-label="Select row"
      />
    ),
    enableSorting: false,
    enableHiding: false
  }
]

Update <DataTable>

data-table.tsx
export function DataTable<TData, TValue>(props: DataTableProps<TData, TValue>) {
  const [sorting, setSorting] = createSignal<SortingState>([])
  const [columnFilters, setColumnFilters] = createSignal<ColumnFiltersState>([])
  const [columnVisibility, setColumnVisibility] = createSignal<VisibilityState>({})
  const [rowSelection, setRowSelection] = createSignal({})
 
  const table = useReactTable({
    get data() {
      return props.data
    },
    get columns() {
      return props.columns
    },
    onSortingChange: setSorting,
    onColumnFiltersChange: setColumnFilters,
    getCoreRowModel: getCoreRowModel(),
    getPaginationRowModel: getPaginationRowModel(),
    getSortedRowModel: getSortedRowModel(),
    getFilteredRowModel: getFilteredRowModel(),
    onColumnVisibilityChange: setColumnVisibility,
    onRowSelectionChange: setRowSelection,
    state: {
      get sorting() {
        return sorting()
      },
      get columnFilters() {
        return columnFilters()
      },
      get columnVisibility() {
        return columnVisibility()
      },
      get rowSelection() {
        return rowSelection()
      }
    }
  })
 
  return (
    <div>
      <div class="rounded-md border">
        <Table />
      </div>
    </div>
  )
}

This adds a checkbox to each row and a checkbox in the header to select all rows.

Show selected rows

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

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

Reusable Components

Check out the Tasks example to learn about creating reusable components for your data tables.