Column Pinning Table
Pin columns to the left or right edge for easy reference while scrolling.
1"use client"2
3import { DataTableRoot } from "@/components/niko-table/core/data-table-root"4import { DataTable } from "@/components/niko-table/core/data-table"5import {6 DataTableHeader,7 DataTableBody,8 DataTableEmptyBody,9} from "@/components/niko-table/core/data-table-structure"10import { DataTableColumnHeader } from "@/components/niko-table/components/data-table-column-header"11import { DataTableColumnTitle } from "@/components/niko-table/components/data-table-column-title"12import { DataTableColumnActions } from "@/components/niko-table/components/data-table-column-actions"13import { DataTableColumnSortOptions } from "@/components/niko-table/components/data-table-column-sort"14import { DataTableColumnPinOptions } from "@/components/niko-table/components/data-table-column-pin"15import { DataTableToolbarSection } from "@/components/niko-table/components/data-table-toolbar-section"16import {17 DataTableEmptyIcon,18 DataTableEmptyMessage,19 DataTableEmptyTitle,20 DataTableEmptyDescription,21} from "@/components/niko-table/components/data-table-empty-state"22import { DataTableSearchFilter } from "@/components/niko-table/components/data-table-search-filter"23import { DataTableViewMenu } from "@/components/niko-table/components/data-table-view-menu"24import { DataTablePagination } from "@/components/niko-table/components/data-table-pagination"25import { FILTER_VARIANTS } from "@/components/niko-table/lib/constants"26import type { DataTableColumnDef } from "@/components/niko-table/types"27import { Badge } from "@/components/ui/badge"28import { Button } from "@/components/ui/button"29import {30 DropdownMenu,31 DropdownMenuContent,32 DropdownMenuItem,33 DropdownMenuLabel,34 DropdownMenuSeparator,35 DropdownMenuTrigger,36} from "@/components/ui/dropdown-menu"37import { MoreHorizontal, PackageSearch } from "lucide-react"38
39// Types40type Order = {41 id: string42 customer: string43 product: string44 amount: number45 status: "pending" | "shipped" | "delivered" | "cancelled"46 date: string47 region: string48}49
50// Sample data with enough columns to demonstrate horizontal scrolling51const data: Order[] = [52 {53 id: "ORD-001",54 customer: "John Doe",55 product: "Premium Widget",56 amount: 299.99,57 status: "delivered",58 date: "2024-01-15",59 region: "North America",60 },61 {62 id: "ORD-002",63 customer: "Jane Smith",64 product: "Basic Kit",65 amount: 149.5,66 status: "shipped",67 date: "2024-01-18",68 region: "Europe",69 },70 {71 id: "ORD-003",72 customer: "Bob Johnson",73 product: "Pro Bundle",74 amount: 599.0,75 status: "pending",76 date: "2024-01-20",77 region: "Asia Pacific",78 },79 {80 id: "ORD-004",81 customer: "Alice Williams",82 product: "Starter Pack",83 amount: 79.99,84 status: "delivered",85 date: "2024-01-22",86 region: "North America",87 },88 {89 id: "ORD-005",90 customer: "Charlie Brown",91 product: "Enterprise Suite",92 amount: 1299.0,93 status: "shipped",94 date: "2024-01-25",95 region: "Europe",96 },97 {98 id: "ORD-006",99 customer: "Diana Prince",100 product: "Premium Widget",101 amount: 299.99,102 status: "cancelled",103 date: "2024-01-28",104 region: "Asia Pacific",105 },106 {107 id: "ORD-007",108 customer: "Ethan Hunt",109 product: "Basic Kit",110 amount: 149.5,111 status: "pending",112 date: "2024-02-01",113 region: "North America",114 },115 {116 id: "ORD-008",117 customer: "Fiona Green",118 product: "Pro Bundle",119 amount: 599.0,120 status: "delivered",121 date: "2024-02-05",122 region: "Europe",123 },124 {125 id: "ORD-009",126 customer: "George Miller",127 product: "Starter Pack",128 amount: 79.99,129 status: "shipped",130 date: "2024-02-08",131 region: "Asia Pacific",132 },133 {134 id: "ORD-010",135 customer: "Hannah Lee",136 product: "Enterprise Suite",137 amount: 1299.0,138 status: "delivered",139 date: "2024-02-12",140 region: "North America",141 },142]143
144// Status badge variant helper145const getStatusVariant = (status: Order["status"]) => {146 switch (status) {147 case "delivered":148 return "default"149 case "shipped":150 return "secondary"151 case "pending":152 return "outline"153 case "cancelled":154 return "destructive"155 default:156 return "secondary"157 }158}159
160// Columns with composable DataTableColumnActions pattern161const columns: DataTableColumnDef<Order>[] = [162 {163 accessorKey: "id",164 size: 110,165 header: () => (166 <DataTableColumnHeader>167 <DataTableColumnTitle title="Order ID" />168 <DataTableColumnActions>169 <DataTableColumnSortOptions />170 <DataTableColumnPinOptions />171 </DataTableColumnActions>172 </DataTableColumnHeader>173 ),174 meta: { label: "Order ID" },175 },176 {177 accessorKey: "customer",178 size: 160,179 header: () => (180 <DataTableColumnHeader>181 <DataTableColumnTitle title="Customer" />182 <DataTableColumnActions>183 <DataTableColumnSortOptions />184 <DataTableColumnPinOptions />185 </DataTableColumnActions>186 </DataTableColumnHeader>187 ),188 meta: { label: "Customer" },189 },190 {191 accessorKey: "product",192 size: 180,193 header: () => (194 <DataTableColumnHeader>195 <DataTableColumnTitle title="Product" />196 <DataTableColumnActions>197 <DataTableColumnSortOptions />198 <DataTableColumnPinOptions />199 </DataTableColumnActions>200 </DataTableColumnHeader>201 ),202 meta: { label: "Product" },203 },204 {205 accessorKey: "amount",206 size: 120,207 meta: { label: "Amount", variant: FILTER_VARIANTS.NUMBER },208 header: () => (209 <DataTableColumnHeader>210 <DataTableColumnTitle title="Amount" />211 <DataTableColumnActions>212 <DataTableColumnSortOptions variant={FILTER_VARIANTS.NUMBER} />213 <DataTableColumnPinOptions />214 </DataTableColumnActions>215 </DataTableColumnHeader>216 ),217 cell: ({ row }) => {218 const amount = parseFloat(row.getValue("amount"))219 return new Intl.NumberFormat("en-US", {220 style: "currency",221 currency: "USD",222 }).format(amount)223 },224 },225 {226 accessorKey: "status",227 size: 120,228 header: () => (229 <DataTableColumnHeader>230 <DataTableColumnTitle title="Status" />231 <DataTableColumnActions>232 <DataTableColumnPinOptions />233 </DataTableColumnActions>234 </DataTableColumnHeader>235 ),236 meta: { label: "Status" },237 cell: ({ row }) => {238 const status = row.getValue("status") as Order["status"]239 return <Badge variant={getStatusVariant(status)}>{status}</Badge>240 },241 },242 {243 accessorKey: "date",244 size: 130,245 meta: { label: "Date", variant: FILTER_VARIANTS.DATE },246 header: () => (247 <DataTableColumnHeader>248 <DataTableColumnTitle title="Date" />249 <DataTableColumnActions>250 <DataTableColumnSortOptions variant={FILTER_VARIANTS.DATE} />251 <DataTableColumnPinOptions />252 </DataTableColumnActions>253 </DataTableColumnHeader>254 ),255 cell: ({ row }) => {256 return new Date(row.getValue("date")).toLocaleDateString()257 },258 },259 {260 accessorKey: "region",261 size: 140,262 header: () => (263 <DataTableColumnHeader>264 <DataTableColumnTitle title="Region" />265 <DataTableColumnActions>266 <DataTableColumnPinOptions />267 </DataTableColumnActions>268 </DataTableColumnHeader>269 ),270 meta: { label: "Region" },271 },272 {273 id: "actions",274 size: 70,275 header: () => (276 <DataTableColumnHeader>277 <DataTableColumnTitle title="" />278 </DataTableColumnHeader>279 ),280 cell: ({ row }) => (281 <DropdownMenu>282 <DropdownMenuTrigger asChild>283 <Button variant="ghost" className="h-8 w-8 p-0">284 <span className="sr-only">Open menu</span>285 <MoreHorizontal className="h-4 w-4" />286 </Button>287 </DropdownMenuTrigger>288 <DropdownMenuContent align="end">289 <DropdownMenuLabel>Actions</DropdownMenuLabel>290 <DropdownMenuItem291 onClick={() => navigator.clipboard.writeText(row.original.id)}292 >293 Copy order ID294 </DropdownMenuItem>295 <DropdownMenuSeparator />296 <DropdownMenuItem>View order</DropdownMenuItem>297 <DropdownMenuItem>Track shipment</DropdownMenuItem>298 </DropdownMenuContent>299 </DropdownMenu>300 ),301 },302]303
304export default function ColumnPinningTable() {305 return (306 <DataTableRoot307 data={data}308 columns={columns}309 initialState={{310 columnPinning: {311 left: ["id"],312 right: ["actions"],313 },314 }}315 >316 <DataTableToolbarSection>317 <DataTableSearchFilter placeholder="Search orders..." />318 <DataTableViewMenu />319 </DataTableToolbarSection>320
321 <DataTable>322 <DataTableHeader />323 <DataTableBody>324 <DataTableEmptyBody>325 <DataTableEmptyMessage>326 <DataTableEmptyIcon>327 <PackageSearch className="size-12" />328 </DataTableEmptyIcon>329 <DataTableEmptyTitle>No orders found</DataTableEmptyTitle>330 <DataTableEmptyDescription>331 Try adjusting your search criteria.332 </DataTableEmptyDescription>333 </DataTableEmptyMessage>334 </DataTableEmptyBody>335 </DataTableBody>336 </DataTable>337
338 <DataTablePagination />339 </DataTableRoot>340 )341}Order ID | Customer | Product | Amount | Status | Date | Region | Actions |
|---|---|---|---|---|---|---|---|
| ORD-001 | John Doe | Premium Widget | $299.99 | delivered | 1/15/2024 | North America | |
| ORD-002 | Jane Smith | Basic Kit | $149.50 | shipped | 1/18/2024 | Europe | |
| ORD-003 | Bob Johnson | Pro Bundle | $599.00 | pending | 1/20/2024 | Asia Pacific | |
| ORD-004 | Alice Williams | Starter Pack | $79.99 | delivered | 1/22/2024 | North America | |
| ORD-005 | Charlie Brown | Enterprise Suite | $1,299.00 | shipped | 1/25/2024 | Europe | |
| ORD-006 | Diana Prince | Premium Widget | $299.99 | cancelled | 1/28/2024 | Asia Pacific | |
| ORD-007 | Ethan Hunt | Basic Kit | $149.50 | pending | 2/1/2024 | North America | |
| ORD-008 | Fiona Green | Pro Bundle | $599.00 | delivered | 2/5/2024 | Europe | |
| ORD-009 | George Miller | Starter Pack | $79.99 | shipped | 2/8/2024 | Asia Pacific | |
| ORD-010 | Hannah Lee | Enterprise Suite | $1,299.00 | delivered | 2/12/2024 | North America |
Preview with Controlled State
1"use client"2
3import { useState } from "react"4import type {5 PaginationState,6 SortingState,7 ColumnPinningState,8 VisibilityState,9} from "@tanstack/react-table"10import { DataTableRoot } from "@/components/niko-table/core/data-table-root"11import { DataTable } from "@/components/niko-table/core/data-table"12import {13 DataTableHeader,14 DataTableBody,15 DataTableEmptyBody,16} from "@/components/niko-table/core/data-table-structure"17import { DataTableColumnHeader } from "@/components/niko-table/components/data-table-column-header"18import { DataTableColumnTitle } from "@/components/niko-table/components/data-table-column-title"19import { DataTableColumnActions } from "@/components/niko-table/components/data-table-column-actions"20import { DataTableColumnSortOptions } from "@/components/niko-table/components/data-table-column-sort"21import { DataTableColumnPinOptions } from "@/components/niko-table/components/data-table-column-pin"22import { DataTableToolbarSection } from "@/components/niko-table/components/data-table-toolbar-section"23import {24 DataTableEmptyIcon,25 DataTableEmptyMessage,26 DataTableEmptyTitle,27 DataTableEmptyDescription,28} from "@/components/niko-table/components/data-table-empty-state"29import { DataTableViewMenu } from "@/components/niko-table/components/data-table-view-menu"30import { DataTableSearchFilter } from "@/components/niko-table/components/data-table-search-filter"31import { DataTablePagination } from "@/components/niko-table/components/data-table-pagination"32import { FILTER_VARIANTS } from "@/components/niko-table/lib/constants"33import type { DataTableColumnDef } from "@/components/niko-table/types"34import { Badge } from "@/components/ui/badge"35import { Button } from "@/components/ui/button"36import {37 Card,38 CardAction,39 CardContent,40 CardDescription,41 CardHeader,42 CardTitle,43} from "@/components/ui/card"44import {45 DropdownMenu,46 DropdownMenuContent,47 DropdownMenuItem,48 DropdownMenuLabel,49 DropdownMenuSeparator,50 DropdownMenuTrigger,51} from "@/components/ui/dropdown-menu"52import { MoreHorizontal, PackageSearch } from "lucide-react"53
54// Types55type Order = {56 id: string57 customer: string58 product: string59 amount: number60 status: "pending" | "shipped" | "delivered" | "cancelled"61 date: string62 region: string63}64
65// Sample data66const data: Order[] = [67 {68 id: "ORD-001",69 customer: "John Doe",70 product: "Premium Widget",71 amount: 299.99,72 status: "delivered",73 date: "2024-01-15",74 region: "North America",75 },76 {77 id: "ORD-002",78 customer: "Jane Smith",79 product: "Basic Kit",80 amount: 149.5,81 status: "shipped",82 date: "2024-01-18",83 region: "Europe",84 },85 {86 id: "ORD-003",87 customer: "Bob Johnson",88 product: "Pro Bundle",89 amount: 599.0,90 status: "pending",91 date: "2024-01-20",92 region: "Asia Pacific",93 },94 {95 id: "ORD-004",96 customer: "Alice Williams",97 product: "Starter Pack",98 amount: 79.99,99 status: "delivered",100 date: "2024-01-22",101 region: "North America",102 },103 {104 id: "ORD-005",105 customer: "Charlie Brown",106 product: "Enterprise Suite",107 amount: 1299.0,108 status: "shipped",109 date: "2024-01-25",110 region: "Europe",111 },112 {113 id: "ORD-006",114 customer: "Diana Prince",115 product: "Premium Widget",116 amount: 299.99,117 status: "cancelled",118 date: "2024-01-28",119 region: "Asia Pacific",120 },121 {122 id: "ORD-007",123 customer: "Ethan Hunt",124 product: "Basic Kit",125 amount: 149.5,126 status: "pending",127 date: "2024-02-01",128 region: "North America",129 },130 {131 id: "ORD-008",132 customer: "Fiona Green",133 product: "Pro Bundle",134 amount: 599.0,135 status: "delivered",136 date: "2024-02-05",137 region: "Europe",138 },139 {140 id: "ORD-009",141 customer: "George Miller",142 product: "Starter Pack",143 amount: 79.99,144 status: "shipped",145 date: "2024-02-08",146 region: "Asia Pacific",147 },148 {149 id: "ORD-010",150 customer: "Hannah Lee",151 product: "Enterprise Suite",152 amount: 1299.0,153 status: "delivered",154 date: "2024-02-12",155 region: "North America",156 },157]158
159// Status badge variant helper160const getStatusVariant = (status: Order["status"]) => {161 switch (status) {162 case "delivered":163 return "default"164 case "shipped":165 return "secondary"166 case "pending":167 return "outline"168 case "cancelled":169 return "destructive"170 default:171 return "secondary"172 }173}174
175// Columns with pinning menu in each header176const columns: DataTableColumnDef<Order>[] = [177 {178 accessorKey: "id",179 size: 110,180 header: () => (181 <DataTableColumnHeader>182 <DataTableColumnTitle title="Order ID" />183 <DataTableColumnActions>184 <DataTableColumnSortOptions />185 <DataTableColumnPinOptions />186 </DataTableColumnActions>187 </DataTableColumnHeader>188 ),189 meta: { label: "Order ID" },190 },191 {192 accessorKey: "customer",193 size: 160,194 header: () => (195 <DataTableColumnHeader>196 <DataTableColumnTitle title="Customer" />197 <DataTableColumnActions>198 <DataTableColumnSortOptions />199 <DataTableColumnPinOptions />200 </DataTableColumnActions>201 </DataTableColumnHeader>202 ),203 meta: { label: "Customer" },204 },205 {206 accessorKey: "product",207 size: 180,208 header: () => (209 <DataTableColumnHeader>210 <DataTableColumnTitle title="Product" />211 <DataTableColumnActions>212 <DataTableColumnSortOptions />213 <DataTableColumnPinOptions />214 </DataTableColumnActions>215 </DataTableColumnHeader>216 ),217 meta: { label: "Product" },218 },219 {220 accessorKey: "amount",221 size: 120,222 meta: { label: "Amount", variant: FILTER_VARIANTS.NUMBER },223 header: () => (224 <DataTableColumnHeader>225 <DataTableColumnTitle title="Amount" />226 <DataTableColumnActions>227 <DataTableColumnSortOptions variant={FILTER_VARIANTS.NUMBER} />228 <DataTableColumnPinOptions />229 </DataTableColumnActions>230 </DataTableColumnHeader>231 ),232 cell: ({ row }) => {233 const amount = parseFloat(row.getValue("amount"))234 return new Intl.NumberFormat("en-US", {235 style: "currency",236 currency: "USD",237 }).format(amount)238 },239 },240 {241 accessorKey: "status",242 size: 120,243 header: () => (244 <DataTableColumnHeader>245 <DataTableColumnTitle title="Status" />246 <DataTableColumnActions>247 <DataTableColumnPinOptions />248 </DataTableColumnActions>249 </DataTableColumnHeader>250 ),251 meta: { label: "Status" },252 cell: ({ row }) => {253 const status = row.getValue("status") as Order["status"]254 return <Badge variant={getStatusVariant(status)}>{status}</Badge>255 },256 },257 {258 accessorKey: "date",259 size: 130,260 meta: { label: "Date", variant: FILTER_VARIANTS.DATE },261 header: () => (262 <DataTableColumnHeader>263 <DataTableColumnTitle title="Date" />264 <DataTableColumnActions>265 <DataTableColumnSortOptions variant={FILTER_VARIANTS.DATE} />266 <DataTableColumnPinOptions />267 </DataTableColumnActions>268 </DataTableColumnHeader>269 ),270 cell: ({ row }) => {271 return new Date(row.getValue("date")).toLocaleDateString()272 },273 },274 {275 accessorKey: "region",276 size: 140,277 header: () => (278 <DataTableColumnHeader>279 <DataTableColumnTitle title="Region" />280 <DataTableColumnActions>281 <DataTableColumnPinOptions />282 </DataTableColumnActions>283 </DataTableColumnHeader>284 ),285 meta: { label: "Region" },286 },287 {288 id: "actions",289 size: 70,290 header: () => (291 <DataTableColumnHeader>292 <DataTableColumnTitle title="" />293 </DataTableColumnHeader>294 ),295 cell: ({ row }) => (296 <DropdownMenu>297 <DropdownMenuTrigger asChild>298 <Button variant="ghost" className="h-8 w-8 p-0">299 <span className="sr-only">Open menu</span>300 <MoreHorizontal className="h-4 w-4" />301 </Button>302 </DropdownMenuTrigger>303 <DropdownMenuContent align="end">304 <DropdownMenuLabel>Actions</DropdownMenuLabel>305 <DropdownMenuItem306 onClick={() => navigator.clipboard.writeText(row.original.id)}307 >308 Copy order ID309 </DropdownMenuItem>310 <DropdownMenuSeparator />311 <DropdownMenuItem>View order</DropdownMenuItem>312 <DropdownMenuItem>Track shipment</DropdownMenuItem>313 </DropdownMenuContent>314 </DropdownMenu>315 ),316 },317]318
319export default function ColumnPinningStateTable() {320 const [globalFilter, setGlobalFilter] = useState<string | object>("")321 const [sorting, setSorting] = useState<SortingState>([])322 const [columnVisibility, setColumnVisibility] = useState<VisibilityState>({})323 const [columnPinning, setColumnPinning] = useState<ColumnPinningState>({324 left: ["id"],325 right: ["actions"],326 })327 const [pagination, setPagination] = useState<PaginationState>({328 pageIndex: 0,329 pageSize: 10,330 })331
332 const resetAllState = () => {333 setGlobalFilter("")334 setSorting([])335 setColumnVisibility({})336 setColumnPinning({ left: [], right: [] })337 setPagination({ pageIndex: 0, pageSize: 10 })338 }339
340 return (341 <div className="w-full space-y-4">342 <DataTableRoot343 data={data}344 columns={columns}345 state={{346 globalFilter,347 sorting,348 columnVisibility,349 columnPinning,350 pagination,351 }}352 onGlobalFilterChange={value => {353 setGlobalFilter(value)354 setPagination(prev => ({ ...prev, pageIndex: 0 }))355 }}356 onSortingChange={setSorting}357 onColumnVisibilityChange={setColumnVisibility}358 onColumnPinningChange={setColumnPinning}359 onPaginationChange={setPagination}360 >361 <DataTableToolbarSection>362 <DataTableSearchFilter placeholder="Search orders..." />363 <DataTableViewMenu />364 </DataTableToolbarSection>365
366 <DataTable>367 <DataTableHeader />368 <DataTableBody>369 <DataTableEmptyBody>370 <DataTableEmptyMessage>371 <DataTableEmptyIcon>372 <PackageSearch className="size-12" />373 </DataTableEmptyIcon>374 <DataTableEmptyTitle>No orders found</DataTableEmptyTitle>375 <DataTableEmptyDescription>376 Try adjusting your search criteria.377 </DataTableEmptyDescription>378 </DataTableEmptyMessage>379 </DataTableEmptyBody>380 </DataTableBody>381 </DataTable>382 <DataTablePagination />383 </DataTableRoot>384
385 {/* State Display for demonstration */}386 <Card>387 <CardHeader>388 <CardTitle>Current Table State</CardTitle>389 <CardDescription>390 Live view of the current table state for demonstration purposes391 </CardDescription>392 <CardAction>393 <Button variant="outline" size="sm" onClick={resetAllState}>394 Reset All State395 </Button>396 </CardAction>397 </CardHeader>398 <CardContent className="space-y-4">399 <div className="grid gap-2 text-xs text-muted-foreground">400 <div className="flex justify-between">401 <span className="font-medium">Search Query:</span>402 <span className="text-foreground">403 {typeof globalFilter === "string"404 ? globalFilter || "None"405 : "Mixed Filters"}406 </span>407 </div>408
409 <div className="flex justify-between">410 <span className="font-medium">Total Items:</span>411 <span className="text-foreground">{data.length}</span>412 </div>413
414 <div className="flex justify-between">415 <span className="font-medium">Sorting:</span>416 <span className="text-foreground">417 {sorting.length > 0418 ? sorting419 .map(s => `${s.id} ${s.desc ? "desc" : "asc"}`)420 .join(", ")421 : "None"}422 </span>423 </div>424
425 <div className="flex justify-between">426 <span className="font-medium">Page:</span>427 <span className="text-foreground">428 {pagination.pageIndex + 1} (Size: {pagination.pageSize})429 </span>430 </div>431
432 <div className="flex justify-between">433 <span className="font-medium">Hidden Columns:</span>434 <span className="text-foreground">435 {436 Object.values(columnVisibility).filter(v => v === false)437 .length438 }439 </span>440 </div>441
442 <div className="flex justify-between">443 <span className="font-medium">Pinned Columns:</span>444 <span className="text-foreground">445 {columnPinning.left?.length || 0} Left,{" "}446 {columnPinning.right?.length || 0} Right447 </span>448 </div>449 </div>450
451 {/* Detailed state (collapsible) */}452 <details className="border-t pt-4">453 <summary className="cursor-pointer text-xs font-medium hover:text-foreground">454 View Full State Object455 </summary>456 <div className="mt-4 space-y-3 text-xs">457 <div>458 <strong>Sorting:</strong>459 <pre className="mt-1 overflow-auto rounded bg-muted p-2">460 {JSON.stringify(sorting, null, 2)}461 </pre>462 </div>463 <div>464 <strong>Column Visibility:</strong>465 <pre className="mt-1 overflow-auto rounded bg-muted p-2">466 {JSON.stringify(columnVisibility, null, 2)}467 </pre>468 </div>469 <div>470 <strong>Column Pinning:</strong>471 <pre className="mt-1 overflow-auto rounded bg-muted p-2">472 {JSON.stringify(columnPinning, null, 2)}473 </pre>474 </div>475 </div>476 </details>477 </CardContent>478 </Card>479 </div>480 )481}Order ID | Customer | Product | Amount | Status | Date | Region | Actions |
|---|---|---|---|---|---|---|---|
| ORD-001 | John Doe | Premium Widget | $299.99 | delivered | 1/15/2024 | North America | |
| ORD-002 | Jane Smith | Basic Kit | $149.50 | shipped | 1/18/2024 | Europe | |
| ORD-003 | Bob Johnson | Pro Bundle | $599.00 | pending | 1/20/2024 | Asia Pacific | |
| ORD-004 | Alice Williams | Starter Pack | $79.99 | delivered | 1/22/2024 | North America | |
| ORD-005 | Charlie Brown | Enterprise Suite | $1,299.00 | shipped | 1/25/2024 | Europe | |
| ORD-006 | Diana Prince | Premium Widget | $299.99 | cancelled | 1/28/2024 | Asia Pacific | |
| ORD-007 | Ethan Hunt | Basic Kit | $149.50 | pending | 2/1/2024 | North America | |
| ORD-008 | Fiona Green | Pro Bundle | $599.00 | delivered | 2/5/2024 | Europe | |
| ORD-009 | George Miller | Starter Pack | $79.99 | shipped | 2/8/2024 | Asia Pacific | |
| ORD-010 | Hannah Lee | Enterprise Suite | $1,299.00 | delivered | 2/12/2024 | North America |
View Full State Object
[]
{}{
"left": [
"id"
],
"right": [
"actions"
]
}Introduction
Section titled “Introduction”Column pinning keeps important columns visible while users scroll horizontally. Pin identifier columns (like Order ID) to the left and action columns to the right.
Installation
Section titled “Installation”Install the DataTable core and add-ons for this example:
pnpm dlx shadcn@latest add @niko-table/data-table @niko-table/data-table-column-pin @niko-table/data-table-column-sort @niko-table/data-table-column-hide @niko-table/data-table-pagination @niko-table/data-table-search-filter @niko-table/data-table-view-menuFirst time using
@niko-table? See the Installation Guide to set up the registry.
For other add-ons or manual copy-paste, see the Installation Guide.
Prerequisites
Section titled “Prerequisites”We’ll build a table showing orders. Here’s our data:
1type Order = {2 id: string3 customer: string4 product: string5 amount: number6 status: "pending" | "shipped" | "delivered" | "cancelled"7 date: string8 region: string9}10
11const data: Order[] = [12 {13 id: "ORD-001",14 customer: "John Doe",15 product: "Premium Widget",16 amount: 299.99,17 status: "delivered",18 date: "2024-01-15",19 region: "North America",20 },21 // ...22]Basic Column Pinning
Section titled “Basic Column Pinning”Set initialState.columnPinning to pin columns on mount:
1<DataTableRoot2 data={data}3 columns={columns}4 initialState={{5 columnPinning: {6 left: ["id"], // Pin Order ID to left7 right: ["actions"], // Pin actions to right8 },9 }}10>11 <DataTable>12 <DataTableHeader />13 <DataTableBody />14 </DataTable>15</DataTableRoot>Column Definitions
Section titled “Column Definitions”Set explicit size for each column to enable horizontal scrolling:
1const columns: DataTableColumnDef<Order>[] = [2 {3 accessorKey: "id",4 size: 110,5 header: () => (6 <DataTableColumnHeader>7 <DataTableColumnTitle title="Order ID" />8 <DataTableColumnSortMenu />9 </DataTableColumnHeader>10 ),11 meta: { label: "Order ID" },12 },13 {14 accessorKey: "customer",15 size: 160,16 header: () => (17 <DataTableColumnHeader>18 <DataTableColumnTitle title="Customer" />19 <DataTableColumnSortMenu />20 </DataTableColumnHeader>21 ),22 meta: { label: "Customer" },23 },24 // ... more columns25]Interactive Pinning
Section titled “Interactive Pinning”Add pinning options to column actions for user-controlled pinning:
1{2 accessorKey: "customer",3 size: 160,4 header: () => (5 <DataTableColumnHeader>6 <DataTableColumnTitle title="Customer" />7 <DataTableColumnActions>8 <DataTableColumnSortOptions />9 <DataTableColumnPinOptions />10 </DataTableColumnActions>11 </DataTableColumnHeader>12 ),13}Controlled State
Section titled “Controlled State”Manage pinning state externally:
1import { useState } from "react"2import { type ColumnPinningState } from "@tanstack/react-table"3
4export function ControlledPinningTable({ data }: { data: Order[] }) {5 const [columnPinning, setColumnPinning] = useState<ColumnPinningState>({6 left: ["id"],7 right: ["actions"],8 })9
10 return (11 <DataTableRoot12 data={data}13 columns={columns}14 state={{ columnPinning }}15 onColumnPinningChange={setColumnPinning}16 >17 {/* ... */}18 </DataTableRoot>19 )20}When to Use
Section titled “When to Use”✅ Use Column Pinning when:
- Tables have many columns requiring horizontal scroll
- Key identifiers (ID, Name) must stay visible
- Actions column should be always accessible
❌ Consider other options when:
- All columns fit on screen (no scrolling needed)
- Mobile-first design (pinning adds complexity)
Next Steps
Section titled “Next Steps”- Virtualization Table - Combine pinning with virtual scrolling
- Advanced Table - Persistent pinning state