Want to skip to the implementation? Check out these Angular examples:
import { signal } from '@angular/core'
import { injectTable, tableFeatures, columnPinningFeature } from '@tanstack/angular-table'
const features = tableFeatures({ columnPinningFeature })
export class App {
readonly data = signal(defaultData)
readonly table = injectTable(() => ({
features,
columns,
data: this.data(),
}))
}import { signal } from '@angular/core'
import { injectTable, tableFeatures, columnPinningFeature } from '@tanstack/angular-table'
const features = tableFeatures({ columnPinningFeature })
export class App {
readonly data = signal(defaultData)
readonly table = injectTable(() => ({
features,
columns,
data: this.data(),
}))
}TanStack Table offers state and APIs helpful for implementing column pinning features in your table UI. You can implement column pinning in multiple ways. You can either split pinned columns into their own separate tables, or you can keep all columns in the same table, but use the pinning state to order the columns correctly and use sticky CSS to pin the columns to the left or right.
There are 3 table features that can reorder columns, which happen in the following order:
The only way to change the order of the pinned columns is in the columnPinning.left and columnPinning.right state itself. columnOrder state will only affect the order of the unpinned ("center") columns.
Managing the columnPinning state is optional, and usually not necessary unless you are adding persistent state features. TanStack Table will already keep track of the column pinning state for you. Manage the columnPinning state just like any other table state if you need to.
In v9, the recommended way to own a state slice is with an external atom (created with createAtom from @tanstack/angular-store) passed to the table's atoms option. External atoms give you fine-grained subscriptions anywhere in your app, and other code can read or write the pinning state without re-running the injectTable options initializer on every change.
import { createAtom } from '@tanstack/angular-store'
import { injectTable, tableFeatures, columnPinningFeature } from '@tanstack/angular-table'
import type { ColumnPinningState } from '@tanstack/angular-table'
const features = tableFeatures({ columnPinningFeature })
export class App {
readonly columnPinningAtom = createAtom<ColumnPinningState>({
left: [],
right: [],
})
readonly table = injectTable(() => ({
features,
//...
atoms: {
columnPinning: this.columnPinningAtom,
},
//...
}))
// read this.columnPinningAtom.get() wherever you need the value
}import { createAtom } from '@tanstack/angular-store'
import { injectTable, tableFeatures, columnPinningFeature } from '@tanstack/angular-table'
import type { ColumnPinningState } from '@tanstack/angular-table'
const features = tableFeatures({ columnPinningFeature })
export class App {
readonly columnPinningAtom = createAtom<ColumnPinningState>({
left: [],
right: [],
})
readonly table = injectTable(() => ({
features,
//...
atoms: {
columnPinning: this.columnPinningAtom,
},
//...
}))
// read this.columnPinningAtom.get() wherever you need the value
}Alternatively, the v8-style state.columnPinning plus onColumnPinningChange pattern is still supported. In Angular this means owning the slice with an Angular signal. It can be convenient for simple integrations or when migrating v8 code, but it is less fine-grained than external atoms. See the Table State Guide for a deeper comparison.
readonly columnPinning = signal<ColumnPinningState>({
left: [],
right: [],
})
readonly table = injectTable(() => ({
features,
//...
state: {
columnPinning: this.columnPinning(),
//...
},
onColumnPinningChange: (updater) =>
typeof updater === 'function'
? this.columnPinning.update(updater)
: this.columnPinning.set(updater),
//...
}))readonly columnPinning = signal<ColumnPinningState>({
left: [],
right: [],
})
readonly table = injectTable(() => ({
features,
//...
state: {
columnPinning: this.columnPinning(),
//...
},
onColumnPinningChange: (updater) =>
typeof updater === 'function'
? this.columnPinning.update(updater)
: this.columnPinning.set(updater),
//...
}))A very common use case is to pin some columns by default. You can do this by either initializing the columnPinning state with the pinned columnIds, or by using the initialState table option
readonly table = injectTable(() => ({
features,
//...
initialState: {
columnPinning: {
left: ['expand-column'],
right: ['actions-column'],
},
//...
},
//...
}))readonly table = injectTable(() => ({
features,
//...
initialState: {
columnPinning: {
left: ['expand-column'],
right: ['actions-column'],
},
//...
},
//...
}))Note: These APIs are available when using columnPinningFeature.
There are a handful of useful Column API methods to help you implement column pinning features:
Use table.setColumnPinning to update the pinning state directly. Use table.resetColumnPinning to reset to initialState.columnPinning, or pass true to clear both pinned column arrays.
table.setColumnPinning({
left: ['firstName'],
right: ['actions'],
})
table.resetColumnPinning()
table.resetColumnPinning(true)table.setColumnPinning({
left: ['firstName'],
right: ['actions'],
})
table.resetColumnPinning()
table.resetColumnPinning(true)The table instance exposes pinned column and header helpers for each region:
table.getLeftLeafColumns()
table.getCenterLeafColumns()
table.getRightLeafColumns()
table.getLeftVisibleLeafColumns()
table.getCenterVisibleLeafColumns()
table.getRightVisibleLeafColumns()
table.getLeftHeaderGroups()
table.getCenterHeaderGroups()
table.getRightHeaderGroups()
table.getLeftFooterGroups()
table.getCenterFooterGroups()
table.getRightFooterGroups()
table.getLeftFlatHeaders()
table.getCenterFlatHeaders()
table.getRightFlatHeaders()
table.getLeftLeafHeaders()
table.getCenterLeafHeaders()
table.getRightLeafHeaders()table.getLeftLeafColumns()
table.getCenterLeafColumns()
table.getRightLeafColumns()
table.getLeftVisibleLeafColumns()
table.getCenterVisibleLeafColumns()
table.getRightVisibleLeafColumns()
table.getLeftHeaderGroups()
table.getCenterHeaderGroups()
table.getRightHeaderGroups()
table.getLeftFooterGroups()
table.getCenterFooterGroups()
table.getRightFooterGroups()
table.getLeftFlatHeaders()
table.getCenterFlatHeaders()
table.getRightFlatHeaders()
table.getLeftLeafHeaders()
table.getCenterLeafHeaders()
table.getRightLeafHeaders()You can also request pinned leaf columns by region with table.getPinnedLeafColumns(position) and visible pinned leaf columns with table.getPinnedVisibleLeafColumns(position).
table.getPinnedLeafColumns('left')
table.getPinnedLeafColumns('center')
table.getPinnedLeafColumns('right')
table.getPinnedVisibleLeafColumns('left')
table.getPinnedVisibleLeafColumns('center')
table.getPinnedVisibleLeafColumns('right')table.getPinnedLeafColumns('left')
table.getPinnedLeafColumns('center')
table.getPinnedLeafColumns('right')
table.getPinnedVisibleLeafColumns('left')
table.getPinnedVisibleLeafColumns('center')
table.getPinnedVisibleLeafColumns('right')Use table.getIsSomeColumnsPinned() to check if any columns are pinned, or pass 'left' or 'right' to check one pinned side.
If you are just using sticky CSS to pin columns, you can for the most part, just render the table as you normally would with the table.getHeaderGroups and row.getVisibleCells methods.
However, if you are splitting up pinned columns into their own separate tables, you can make use of the table.getLeftHeaderGroups, table.getCenterHeaderGroups, table.getRightHeaderGroups, row.getLeftVisibleCells, row.getCenterVisibleCells, and row.getRightVisibleCells methods to only render the columns that are relevant to the current table.