Docs
Cloudflare
Railway
CodeRabbit
Clerk
AG Grid
Netlify
WorkOS
SerpAPI
OpenRouter
Sentry
Electric
Unkey
Prisma
Cloudflare
Railway
CodeRabbit
Clerk
AG Grid
Netlify
WorkOS
SerpAPI
OpenRouter
Sentry
Electric
Unkey
Prisma
Table API Reference
Column API Reference
Row API Reference
Cell API Reference
Header API Reference
Features API Reference
Static Functions API Reference
Getting Started

Quick Start

TanStack Table is a headless table library. It manages your table's state and logic (sorting, filtering, pagination, selection, and more) while you keep 100% control over the markup and styles. This page gets you from install to a rendering Angular table, then shows how to layer on your first feature.

Installation

TanStack Table v9 is currently published under the beta tag:

sh
npm install @tanstack/angular-table@beta
npm install @tanstack/angular-table@beta

Your First Table

The component and template below are complete. Drop them into an Angular app and you will see a working table.

ts
// app.ts
import { ChangeDetectionStrategy, Component, signal } from '@angular/core'
import { FlexRender, injectTable, tableFeatures } from '@tanstack/angular-table'
import type { ColumnDef } from '@tanstack/angular-table'

// 1. Define the shape of your data
type Person = {
  firstName: string
  lastName: string
  age: number
}

// 2. Create some data with a stable reference
const defaultData: Array<Person> = [
  { firstName: 'tanner', lastName: 'linsley', age: 24 },
  { firstName: 'tandy', lastName: 'miller', age: 40 },
  { firstName: 'joe', lastName: 'dirte', age: 45 },
]

// 3. New in v9: declare which features this table uses (none yet)
const features = tableFeatures({})

// 4. Define your columns
const columns: Array<ColumnDef<typeof features, Person>> = [
  {
    accessorKey: 'firstName', // accessorKey shorthand
    header: 'First Name',
    cell: (info) => info.getValue(),
  },
  {
    accessorFn: (row) => row.lastName, // accessorFn alternative with a custom id
    id: 'lastName',
    header: () => 'Last Name',
    cell: (info) => info.getValue<string>(),
  },
  {
    accessorKey: 'age',
    header: () => 'Age',
  },
]

@Component({
  selector: 'app-root',
  imports: [FlexRender],
  templateUrl: './app.html',
  changeDetection: ChangeDetectionStrategy.OnPush,
})
export class App {
  // 5. Own the data with a signal so updates flow into the table
  readonly data = signal<Array<Person>>([...defaultData])

  // 6. Create the table instance
  readonly table = injectTable(() => ({
    key: 'person-table', // registers this table with the devtools
    features,
    columns,
    data: this.data(),
  }))
}
// app.ts
import { ChangeDetectionStrategy, Component, signal } from '@angular/core'
import { FlexRender, injectTable, tableFeatures } from '@tanstack/angular-table'
import type { ColumnDef } from '@tanstack/angular-table'

// 1. Define the shape of your data
type Person = {
  firstName: string
  lastName: string
  age: number
}

// 2. Create some data with a stable reference
const defaultData: Array<Person> = [
  { firstName: 'tanner', lastName: 'linsley', age: 24 },
  { firstName: 'tandy', lastName: 'miller', age: 40 },
  { firstName: 'joe', lastName: 'dirte', age: 45 },
]

// 3. New in v9: declare which features this table uses (none yet)
const features = tableFeatures({})

// 4. Define your columns
const columns: Array<ColumnDef<typeof features, Person>> = [
  {
    accessorKey: 'firstName', // accessorKey shorthand
    header: 'First Name',
    cell: (info) => info.getValue(),
  },
  {
    accessorFn: (row) => row.lastName, // accessorFn alternative with a custom id
    id: 'lastName',
    header: () => 'Last Name',
    cell: (info) => info.getValue<string>(),
  },
  {
    accessorKey: 'age',
    header: () => 'Age',
  },
]

@Component({
  selector: 'app-root',
  imports: [FlexRender],
  templateUrl: './app.html',
  changeDetection: ChangeDetectionStrategy.OnPush,
})
export class App {
  // 5. Own the data with a signal so updates flow into the table
  readonly data = signal<Array<Person>>([...defaultData])

  // 6. Create the table instance
  readonly table = injectTable(() => ({
    key: 'person-table', // registers this table with the devtools
    features,
    columns,
    data: this.data(),
  }))
}
html
<!-- app.html -->
<table>
  <thead>
    @for (headerGroup of table.getHeaderGroups(); track headerGroup.id) {
      <tr>
        @for (header of headerGroup.headers; track header.id) {
          <th>
            @if (!header.isPlaceholder) {
              <ng-container *flexRenderHeader="header; let headerCell">
                {{ headerCell }}
              </ng-container>
            }
          </th>
        }
      </tr>
    }
  </thead>
  <tbody>
    @for (row of table.getRowModel().rows; track row.id) {
      <tr>
        @for (cell of row.getAllCells(); track cell.id) {
          <td>
            <ng-container *flexRenderCell="cell; let renderCell">
              {{ renderCell }}
            </ng-container>
          </td>
        }
      </tr>
    }
  </tbody>
</table>
<!-- app.html -->
<table>
  <thead>
    @for (headerGroup of table.getHeaderGroups(); track headerGroup.id) {
      <tr>
        @for (header of headerGroup.headers; track header.id) {
          <th>
            @if (!header.isPlaceholder) {
              <ng-container *flexRenderHeader="header; let headerCell">
                {{ headerCell }}
              </ng-container>
            }
          </th>
        }
      </tr>
    }
  </thead>
  <tbody>
    @for (row of table.getRowModel().rows; track row.id) {
      <tr>
        @for (cell of row.getAllCells(); track cell.id) {
          <td>
            <ng-container *flexRenderCell="cell; let renderCell">
              {{ renderCell }}
            </ng-container>
          </td>
        }
      </tr>
    }
  </tbody>
</table>

A few things to note:

  • tableFeatures({}) declares which optional features the table uses. Registering only what you need keeps bundles small and gives TypeScript accurate types for the table instance.
  • injectTable must be called in an injection context. Its initializer re-runs when Angular signals read inside it change (like this.data() here), and the adapter syncs the table options.
  • The FlexRender directives (*flexRenderHeader, *flexRenderCell, *flexRenderFooter) render the header, cell, and footer definitions from your columns, whether they are plain values, templates, or components. See the Rendering Guide for flexRenderComponent and render context helpers.
  • The key option is optional unless you use the TanStack Table Devtools. The devtools identify tables by key, and you register a table with injectTanStackTableDevtools from @tanstack/angular-table-devtools.

See the full Basic injectTable example for a runnable version with more columns and a footer.

Add a Feature: Sorting

Features are opt-in in v9. To make columns sortable, register rowSortingFeature and the sorted row model in tableFeatures, then wire the header click handler in the template.

ts
// app.ts
import {
  createSortedRowModel,
  rowSortingFeature,
  sortFns,
  tableFeatures,
} from '@tanstack/angular-table'

const features = tableFeatures({
  rowSortingFeature, // enables sorting APIs and state
  sortedRowModel: createSortedRowModel(), // client-side sorting
  sortFns,
})

export class App {
  readonly data = signal<Array<Person>>([...defaultData])

  readonly table = injectTable(() => ({
    key: 'person-table',
    features,
    columns,
    data: this.data(),
  }))

  sortIndicator(sortDirection: false | 'asc' | 'desc') {
    if (sortDirection === 'asc') return ' 🔼'
    if (sortDirection === 'desc') return ' 🔽'
    return null
  }
}
// app.ts
import {
  createSortedRowModel,
  rowSortingFeature,
  sortFns,
  tableFeatures,
} from '@tanstack/angular-table'

const features = tableFeatures({
  rowSortingFeature, // enables sorting APIs and state
  sortedRowModel: createSortedRowModel(), // client-side sorting
  sortFns,
})

export class App {
  readonly data = signal<Array<Person>>([...defaultData])

  readonly table = injectTable(() => ({
    key: 'person-table',
    features,
    columns,
    data: this.data(),
  }))

  sortIndicator(sortDirection: false | 'asc' | 'desc') {
    if (sortDirection === 'asc') return ' 🔼'
    if (sortDirection === 'desc') return ' 🔽'
    return null
  }
}
html
<!-- app.html: thead changes, tbody unchanged from above -->
<thead>
  @for (headerGroup of table.getHeaderGroups(); track headerGroup.id) {
    <tr>
      @for (header of headerGroup.headers; track header.id) {
        <th>
          @if (!header.isPlaceholder) {
            <div
              [style.cursor]="header.column.getCanSort() ? 'pointer' : null"
              (click)="header.column.getToggleSortingHandler()?.($event)"
            >
              <ng-container *flexRenderHeader="header; let headerCell">
                {{ headerCell }}
              </ng-container>
              {{ sortIndicator(header.column.getIsSorted()) }}
            </div>
          }
        </th>
      }
    </tr>
  }
</thead>
<!-- app.html: thead changes, tbody unchanged from above -->
<thead>
  @for (headerGroup of table.getHeaderGroups(); track headerGroup.id) {
    <tr>
      @for (header of headerGroup.headers; track header.id) {
        <th>
          @if (!header.isPlaceholder) {
            <div
              [style.cursor]="header.column.getCanSort() ? 'pointer' : null"
              (click)="header.column.getToggleSortingHandler()?.($event)"
            >
              <ng-container *flexRenderHeader="header; let headerCell">
                {{ headerCell }}
              </ng-container>
              {{ sortIndicator(header.column.getIsSorted()) }}
            </div>
          }
        </th>
      }
    </tr>
  }
</thead>

Clicking a header now toggles between ascending, descending, and unsorted. Every other feature follows this same pattern: register the feature and its row model factory (when it has one) inside tableFeatures, then use the APIs it adds to the table, columns, and rows. See the Sorting Guide and the Sorting example for custom sort functions, multi-sorting, and per-column options.

Where to Go Next

Table state. In v9, table state is backed by TanStack Store atoms, and table atoms are bridged to Angular signals for you. You usually do not need to manage state yourself: set initialState for starting values and call feature APIs like table.setSorting(...) or table.nextPage(). When your app should own a state slice, or you want fine-grained subscriptions, read the Table State Guide. It is the foundational guide for everything else.

Feature guides. Each feature has its own guide, such as Column Filtering, Pagination, Row Selection, and Column Visibility.

Composable tables. When multiple tables in your app share features, row models, and component conventions, define them once with createTableHook:

ts
const features = tableFeatures({
  rowSortingFeature,
  sortedRowModel: createSortedRowModel(),
  sortFns,
})

const { injectAppTable, createAppColumnHelper } = createTableHook({ features })
const features = tableFeatures({
  rowSortingFeature,
  sortedRowModel: createSortedRowModel(),
  sortFns,
})

const { injectAppTable, createAppColumnHelper } = createTableHook({ features })

See the Composable Tables Guide for the full pattern, including pre-bound cell and header components.

Examples. Browse the runnable Angular examples, from basic tables to feature demos, to see intended usage end to end.