You're reviewing sales data from the past quarter. There are 30+ columns of metrics—conversion rates, revenue per channel, customer acquisition costs, regional breakdowns. You scroll right to see Q3 data, and suddenly you've lost context. Which customer is this row about? What's the deal ID?
This is why column pinning (also called "freezing" or "sticky columns") exists. It keeps key columns—like IDs, names, or action buttons—fixed in place while the rest of the table scrolls horizontally. Users never lose context, no matter how wide your data gets.
In this guide, we'll cover how to implement column pinning in React data grids, common pitfalls to avoid, and best practices for deciding what to pin. Whether you're building financial dashboards, CRM tools, or admin panels, column pinning is essential for working with wide datasets.
Why Column Pinning Matters
Wide tables are everywhere in business software. Financial reports span quarters. Product catalogs have dozens of specs. User tables track countless attributes. Without column pinning, users face a frustrating choice: sacrifice screen space to see everything, or lose context when scrolling.
Common Use Cases
Financial Dashboards
Pin the account name and ID while scrolling through quarterly revenue, expenses, and profit across multiple periods.
CRM & Sales Tools
Keep contact name and company visible while viewing deal stage, value, probability, and activity history across wide tables.
E-Commerce Admin
Pin product name and SKU while managing inventory, pricing, variants, and shipping details across multiple warehouses.
Action Columns
Pin action buttons (Edit, Delete, View) to the right edge so they're always accessible, no matter where users scroll.
The Implementation Challenge
Column pinning sounds simple: just add position: sticky to some columns, right? Unfortunately, the CSS-only approach falls apart quickly. This is why professional data grids like AG Grid and Simple Table handle it for you:
CSS-Only Approach Problems
- Stacking context issues: Z-index wars with other sticky elements like headers or filters
- Shadow/border gaps: Visual artifacts where pinned columns meet scrolling content
- Browser inconsistencies: Different rendering behavior across Chrome, Firefox, Safari
- Touch device quirks: Sticky columns behave unpredictably on mobile/tablet
Library-Based Solution Benefits
- Tested across browsers: Works consistently in all modern browsers
- Handles edge cases: Column resizing, reordering, and dynamic visibility all work together
- Proper shadows/borders: Visual indicators show where pinned columns end
- Mobile-optimized: Touch-friendly behavior on all devices
How to Implement Column Pinning in Simple Table
With Simple Table, column pinning is refreshingly simple. Add a pinned property to any column header, and it stays fixed while the rest scrolls.
Basic Example: CRM Dashboard
1import { SimpleTable, HeaderObject } from "simple-table-core";2import "simple-table-core/styles.css";34const headers: HeaderObject[] = [5 {6 accessor: "id",7 label: "ID",8 width: 80,9 pinned: "left", // Pin to the left10 isSortable: true,11 type: "number",12 },13 {14 accessor: "name",15 label: "Contact Name",16 width: 200,17 pinned: "left", // Also pin to the left18 isSortable: true,19 type: "string",20 },21 {22 accessor: "company",23 label: "Company",24 width: 180,25 isSortable: true,26 type: "string",27 },28 {29 accessor: "dealStage",30 label: "Deal Stage",31 width: 150,32 isSortable: true,33 type: "string",34 },35 {36 accessor: "dealValue",37 label: "Deal Value",38 width: 120,39 isSortable: true,40 type: "number",41 align: "right",42 valueFormatter: ({ value }) =>43 new Intl.NumberFormat('en-US', {44 style: 'currency',45 currency: 'USD'46 }).format(value as number),47 },48 {49 accessor: "probability",50 label: "Probability",51 width: 100,52 isSortable: true,53 type: "number",54 align: "right",55 },56 {57 accessor: "lastContact",58 label: "Last Contact",59 width: 140,60 isSortable: true,61 type: "date",62 },63 {64 accessor: "actions",65 label: "Actions",66 width: 120,67 pinned: "right", // Pin to the right68 align: "center",69 cellRenderer: ({ row }) => (70 <div className="flex gap-2 justify-center">71 <button className="text-blue-600 hover:text-blue-800">Edit</button>72 <button className="text-green-600 hover:text-green-800">View</button>73 </div>74 ),75 },76];7778export default function CRMTable({ data }) {79 return (80 <SimpleTable81 defaultHeaders={headers}82 rows={data}83 rowIdAccessor="id"84 height="500px"85 theme="light"86 />87 );88}
That's it! The ID, Contact Name, and Actions columns stay fixed. Users can scroll through all the deal details while always knowing which contact they're looking at and having quick access to actions.
Key Points
- Left pinning: Use
pinned: "left"for identity columns (ID, name, title) - Right pinning: Use
pinned: "right"for action columns (Edit, Delete, View) - Multiple columns: You can pin multiple columns to the same side—they stack in order
- Works with other features: Pinned columns support sorting, filtering, custom renderers, and all other column features
Column Pinning Best Practices
What to Pin
Good Candidates for Pinning
- • Identity columns: ID, name, title, email
- • Primary context: Customer name, account ID, order number
- • Action buttons: Edit, Delete, View, More
- • Status indicators: Active/Inactive, Deal Stage, Priority
- • Selection checkboxes: For bulk operations
Avoid Pinning These
- • Wide columns: Long descriptions or multi-paragraph content
- • Optional metadata: Created date, last updated, internal notes
- • Too many columns: Don't pin more than 2-3 on each side
- • Columns users rarely need: Advanced technical details
- • Redundant information: If it's also in a detail view
Performance & UX Guidelines
📏 Keep Pinned Columns Narrow
Pinned columns consume valuable horizontal space. Aim for 200-300px total for left-pinned columns, 120-150px for right-pinned actions. Users should see unpinned content without scrolling.
🎯 Pin What's Essential, Not Everything
If you pin 10 columns, users can't see any scrolling content. Pin only what provides context or critical actions. Aim for 1-3 pinned columns max per side.
📱 Test on Mobile & Tablets
On small screens, pinned columns consume a larger percentage of viewport width. Consider conditionally disabling pinning on mobile, or only pin 1 column instead of 2-3.
💡 Use Visual Indicators
Simple Table automatically adds shadows where pinned columns meet scrolling content, helping users understand the layout. Don't remove these—they're critical for usability.
Advanced Patterns
Conditional Pinning (Mobile-Responsive)
You can conditionally pin columns based on screen size using responsive header configuration:
1import { useMediaQuery } from "react-responsive";23export default function ResponsiveTable({ data }) {4 const isMobile = useMediaQuery({ maxWidth: 768 });56 const headers: HeaderObject[] = [7 {8 accessor: "id",9 label: "ID",10 width: 60,11 pinned: isMobile ? undefined : "left", // Only pin on desktop12 },13 {14 accessor: "name",15 label: "Name",16 width: 180,17 pinned: "left", // Always pinned18 },19 // ... other columns20 {21 accessor: "actions",22 label: "Actions",23 width: 100,24 pinned: isMobile ? undefined : "right", // Only pin on desktop25 },26 ];2728 return <SimpleTable defaultHeaders={headers} rows={data} />;29}
User-Configurable Pinning
For power users, let them choose which columns to pin:
1import { useState } from "react";23export default function ConfigurableTable({ data }) {4 const [pinnedColumns, setPinnedColumns] = useState<string[]>(["id", "name"]);56 const headers: HeaderObject[] = [7 {8 accessor: "id",9 label: "ID",10 width: 60,11 pinned: pinnedColumns.includes("id") ? "left" : undefined,12 },13 {14 accessor: "name",15 label: "Name",16 width: 180,17 pinned: pinnedColumns.includes("name") ? "left" : undefined,18 },19 // ... other columns20 ];2122 const togglePin = (accessor: string) => {23 setPinnedColumns((prev) =>24 prev.includes(accessor)25 ? prev.filter((col) => col !== accessor)26 : [...prev, accessor]27 );28 };2930 return (31 <div>32 <div className="mb-4">33 <strong>Pin/Unpin Columns:</strong>34 <button onClick={() => togglePin("id")}>35 {pinnedColumns.includes("id") ? "Unpin" : "Pin"} ID36 </button>37 <button onClick={() => togglePin("name")}>38 {pinnedColumns.includes("name") ? "Unpin" : "Pin"} Name39 </button>40 </div>41 <SimpleTable defaultHeaders={headers} rows={data} />42 </div>43 );44}
Column Pinning Across React Libraries
How does column pinning compare across popular React data grid libraries?
| Library | Pinning Support | API Complexity | Cost |
|---|---|---|---|
| Simple Table | ✓ Built-in | Simple (1 prop) | Free |
| AG Grid Community | ✗ Enterprise Only | N/A | $999+/dev/year |
| TanStack Table | ⚡ Headless | Complex (build UI) | Free |
| Ant Design Table | ✓ Built-in (fixed) | Simple | Free |
| Material React Table | ✓ Built-in | Medium (config) | Free |
Wrap Up: Column Pinning Done Right
Column pinning is essential for working with wide datasets. It keeps users oriented, reduces cognitive load, and makes action buttons always accessible. The implementation doesn't have to be complex—with the right library, it's as simple as adding pinned: "left" or pinned: "right".
- Pin identity columns (ID, name) to the left
- Pin action buttons to the right for easy access
- Keep pinned columns narrow (200-300px total)
- Test on mobile and adjust pinning for small screens
- Don't over-pin—aim for 1-3 columns per side maximum
Whether you're building financial dashboards, CRM tools, or e-commerce admin panels, column pinning improves UX dramatically. With Simple Table, you get production-ready pinning without the complexity—just one prop and you're done.