Column Pinning
Pin columns to the left edge of the grid so they remain visible while the user scrolls horizontally through the remaining columns.
Live Demo
The demo above uses Radix UI for styling. To see this feature with your framework's design system (Fluent UI, Material UI, Vuetify, PrimeNG, etc.), click "Open in online demo" below the demo.
Quick Example
- React
- Angular
- Vue
- Vanilla JS
import { OGrid } from '@alaarab/ogrid-react-radix';
import type { IColumnDef } from '@alaarab/ogrid-react-radix';
interface Person {
id: number;
name: string;
email: string;
department: string;
salary: number;
status: string;
age: number;
startDate: string;
}
const columns: IColumnDef<Person>[] = [
{ columnId: 'name', name: 'Name', pinned: 'left', defaultWidth: 160 },
{ columnId: 'email', name: 'Email', defaultWidth: 220 },
{ columnId: 'department', name: 'Department', defaultWidth: 160 },
{
columnId: 'salary',
name: 'Salary',
type: 'numeric',
defaultWidth: 120,
valueFormatter: (v) => `$${Number(v).toLocaleString()}`,
},
{ columnId: 'status', name: 'Status', defaultWidth: 120 },
{ columnId: 'age', name: 'Age', type: 'numeric', defaultWidth: 80 },
{ columnId: 'startDate', name: 'Start Date', defaultWidth: 130 },
];
function App() {
return (
<OGrid
columns={columns}
data={people}
getRowId={(p) => p.id}
defaultPageSize={10}
/>
);
}
The OGrid component has the same props across all React UI packages. To switch, just change the import:
- Radix (lightweight, default):
from '@alaarab/ogrid-react-radix' - Fluent UI (Microsoft 365 / SPFx):
from '@alaarab/ogrid-react-fluent'— wrap in<FluentProvider> - Material UI (MUI v7):
from '@alaarab/ogrid-react-material'— wrap in<ThemeProvider>
import { Component } from '@angular/core';
import { OGridComponent, type IColumnDef } from '@alaarab/ogrid-angular-material';
@Component({
standalone: true,
imports: [OGridComponent],
template: `<ogrid [props]="gridProps" />`
})
export class GridComponent {
gridProps = {
columns: [
{ columnId: 'name', name: 'Name', pinned: 'left', defaultWidth: 160 },
{ columnId: 'email', name: 'Email', defaultWidth: 220 },
{ columnId: 'department', name: 'Department', defaultWidth: 160 },
{
columnId: 'salary', name: 'Salary', type: 'numeric', defaultWidth: 120,
valueFormatter: (v: unknown) => `$${Number(v).toLocaleString()}`,
},
{ columnId: 'status', name: 'Status', defaultWidth: 120 },
{ columnId: 'age', name: 'Age', type: 'numeric', defaultWidth: 80 },
{ columnId: 'startDate', name: 'Start Date', defaultWidth: 130 },
] as IColumnDef<Person>[],
data: people,
getRowId: (item: Person) => item.id,
};
}
Same component API across Angular packages. To switch, just change the import:
- Radix (CDK):
from '@alaarab/ogrid-angular-radix'(default, lightweight) - Angular Material:
from '@alaarab/ogrid-angular-material' - PrimeNG:
from '@alaarab/ogrid-angular-primeng'
All components are standalone — no NgModule required.
<script setup lang="ts">
import { OGrid, type IColumnDef } from '@alaarab/ogrid-vue-vuetify';
const columns: IColumnDef<Person>[] = [
{ columnId: 'name', name: 'Name', pinned: 'left', defaultWidth: 160 },
{ columnId: 'email', name: 'Email', defaultWidth: 220 },
{ columnId: 'department', name: 'Department', defaultWidth: 160 },
{
columnId: 'salary', name: 'Salary', type: 'numeric', defaultWidth: 120,
valueFormatter: (v) => `$${Number(v).toLocaleString()}`,
},
{ columnId: 'status', name: 'Status', defaultWidth: 120 },
{ columnId: 'age', name: 'Age', type: 'numeric', defaultWidth: 80 },
{ columnId: 'startDate', name: 'Start Date', defaultWidth: 130 },
];
const gridProps = {
columns,
data: people,
getRowId: (item) => item.id,
};
</script>
<template>
<OGrid :gridProps="gridProps" />
</template>
Same component API across Vue packages. To switch, just change the import:
- Radix (Headless UI):
from '@alaarab/ogrid-vue-radix'(default, lightweight) - Vuetify:
from '@alaarab/ogrid-vue-vuetify'— wrap in<v-app>for theming - PrimeVue:
from '@alaarab/ogrid-vue-primevue'
import { OGrid } from '@alaarab/ogrid-js';
import '@alaarab/ogrid-js/styles';
const grid = new OGrid(document.getElementById('grid'), {
columns: [
{ columnId: 'name', name: 'Name', pinned: 'left', defaultWidth: 160 },
{ columnId: 'email', name: 'Email', defaultWidth: 220 },
{ columnId: 'department', name: 'Department', defaultWidth: 160 },
{
columnId: 'salary',
name: 'Salary',
type: 'numeric',
defaultWidth: 120,
valueFormatter: (v) => `$${Number(v).toLocaleString()}`,
},
{ columnId: 'status', name: 'Status', defaultWidth: 120 },
{ columnId: 'age', name: 'Age', type: 'numeric', defaultWidth: 80 },
{ columnId: 'startDate', name: 'Start Date', defaultWidth: 130 },
],
data: people,
getRowId: (p) => p.id,
pageSize: 10,
});
The "Name" column stays fixed on the left while all other columns scroll normally.
How It Works
Set pinned: 'left' on any IColumnDef to pin it. The grid renders pinned columns with CSS position: sticky and a calculated left offset so multiple pinned columns stack correctly.
{ columnId: 'name', name: 'Name', pinned: 'left' }
Pinned columns:
- Use sticky positioning so they stay visible during horizontal scroll.
- Stack left-to-right in the order they appear in the
columnsarray. - Work with all grid features: sorting, filtering, editing, cell selection, and context menu.
With Row Selection
When rowSelection is 'single' or 'multiple', OGrid inserts a checkbox column at the far left. Pinned columns appear after the checkbox column, and both stay visible during scroll.
<OGrid
columns={columns}
data={rows}
getRowId={(r) => r.id}
rowSelection="multiple"
/>
With Column Groups
Pinning works inside column groups. Set pinned: 'left' on the leaf IColumnDef items within a group. The group header will span correctly over the pinned columns.
const columns = [
{
headerName: 'Identity',
children: [
{ columnId: 'id', name: 'ID', pinned: 'left' },
{ columnId: 'name', name: 'Name', pinned: 'left' },
],
},
{ columnId: 'email', name: 'Email' },
];
Persisting Pin State
Pin state can be saved and restored via the Grid API, just like column widths and sort order:
const gridRef = useRef<IOGridApi<Row>>(null);
// Save pin state (e.g. to localStorage)
const state = gridRef.current.getColumnState();
// state.pinnedColumns → { name: 'left' }
localStorage.setItem('gridState', JSON.stringify(state));
// Restore pin state on mount
const saved = JSON.parse(localStorage.getItem('gridState'));
gridRef.current.applyColumnState(saved);
onColumnPinned Callback
Listen for pin changes to persist state automatically:
<OGrid
columns={columns}
data={rows}
getRowId={(r) => r.id}
ref={gridRef}
onColumnPinned={(columnId, pinned) => {
// pinned is 'left', 'right', or null (unpinned)
const state = gridRef.current.getColumnState();
localStorage.setItem('gridState', JSON.stringify(state));
}}
/>
Props Reference
| Type | Field | Values | Description |
|---|---|---|---|
IColumnMeta | pinned | 'left' | 'right' | Pin column to left or right edge |
IOGridProps | onColumnPinned | (columnId, pinned) => void | Called when a column is pinned or unpinned |
IGridColumnState | pinnedColumns | Record<string, 'left' | 'right'> | Pinned columns in saved state |
IColumnMeta | minWidth | number | Minimum width in pixels (useful for pinned columns) |
IColumnMeta | defaultWidth | number | Initial width in pixels |
Pin identifying columns like "Name" or "ID" so users always know which row they are looking at, even when scrolling through many columns.
Related
- Column Groups -- group headers with pinned leaf columns
- Column Chooser -- show/hide columns (including pinned ones)
- Keyboard Navigation -- arrow keys work across pinned and scrollable columns