# Pinned Columns Plugin

> Pin columns to the left or right side of the grid.

The Pinned Columns plugin freezes columns to the left or right edge of the grid—essential for keeping key identifiers or action buttons visible while scrolling through wide datasets. Just set `pinned: 'left'` or `pinned: 'right'` on your column definitions and the plugin handles the rest.

## Installation

```ts
import '@toolbox-web/grid/features/pinned-columns';
```

## Basic Usage

Pin important columns like ID on the left and action buttons on the right. The pinned columns stay fixed while the middle content scrolls horizontally.

#### TypeScript

```ts
import { queryGrid } from '@toolbox-web/grid';

const grid = queryGrid('tbw-grid');
grid.gridConfig = {
  columns: [
    { field: 'id', header: 'ID', pinned: 'left', width: 80 },
    { field: 'name', header: 'Name' },
    { field: 'email', header: 'Email' },
    { field: 'department', header: 'Department' },
    { field: 'salary', header: 'Salary', type: 'currency' },
    { field: 'actions', header: 'Actions', pinned: 'right', width: 120 },
  ],
  features: { pinnedColumns: true },
};
```

#### React

```tsx
import '@toolbox-web/grid-react/features/pinned-columns';
import { DataGrid } from '@toolbox-web/grid-react';

function EmployeeGrid({ employees }) {
  return (
    <DataGrid
      rows={employees}
      columns={[
        { field: 'id', header: 'ID', pinned: 'left', width: 80 },
        { field: 'name', header: 'Name' },
        { field: 'email', header: 'Email' },
        { field: 'department', header: 'Department' },
        { field: 'salary', header: 'Salary', type: 'currency' },
        { field: 'actions', header: 'Actions', pinned: 'right', width: 120 },
      ]}
      pinnedColumns
    />
  );
}
```

#### Vue

```html
<script setup>
import '@toolbox-web/grid-vue/features/pinned-columns';
import { TbwGrid } from '@toolbox-web/grid-vue';

const employees = [
  { id: 1, name: 'Alice', email: 'alice@example.com', department: 'Engineering', salary: 95000 },
  { id: 2, name: 'Bob', email: 'bob@example.com', department: 'Marketing', salary: 75000 },
];

const columns = [
  { field: 'id', header: 'ID', pinned: 'left', width: 80 },
  { field: 'name', header: 'Name' },
  { field: 'email', header: 'Email' },
  { field: 'department', header: 'Department' },
  { field: 'salary', header: 'Salary', type: 'currency' },
  { field: 'actions', header: 'Actions', pinned: 'right', width: 120 },
];
</script>

<template>
  <TbwGrid :rows="employees" :columns="columns" pinned-columns />
</template>
```

#### Angular

```typescript
// Feature import - enables the [pinnedColumns] input
import { GridPinnedColumnsDirective } from '@toolbox-web/grid-angular/features/pinned-columns';
import { Component } from '@angular/core';
import { Grid } from '@toolbox-web/grid-angular';
import type { ColumnConfig } from '@toolbox-web/grid';

@Component({
  selector: 'app-employee-grid',
  imports: [Grid, GridPinnedColumnsDirective],
  template: `
    <tbw-grid
      [rows]="rows"
      [columns]="columns"
      [pinnedColumns]="true"
      style="height: 400px; display: block;">
    </tbw-grid>
  `,
})
export class EmployeeGridComponent {
  rows = [];

  columns: ColumnConfig[] = [
    { field: 'id', header: 'ID', pinned: 'left', width: 80 },
    { field: 'name', header: 'Name' },
    { field: 'email', header: 'Email' },
    { field: 'department', header: 'Department' },
    { field: 'salary', header: 'Salary', type: 'currency' },
    { field: 'actions', header: 'Actions', pinned: 'right', width: 120 },
  ];
}
```

## Demo

```ts
// PinnedColumnsDefaultDemo.astro
import '@toolbox-web/grid';
import { queryGrid } from '@toolbox-web/grid';
import '@toolbox-web/grid/features/pinned-columns';

const container = document.getElementById('pinned-columns-default-demo');
if (container) {
  const FIRST_NAMES = ['Alice', 'Bob', 'Carol', 'Dan', 'Eve', 'Frank', 'Grace', 'Henry', 'Ivy', 'Jack'];
  const LAST_NAMES = ['Johnson', 'Smith', 'Williams', 'Brown', 'Jones', 'Davis', 'Miller', 'Wilson', 'Moore', 'Taylor'];
  const DEPARTMENTS = ['Engineering', 'Marketing', 'Sales', 'HR', 'Finance', 'Legal', 'Support', 'Design'];
  const CITIES = ['New York', 'Los Angeles', 'Chicago', 'Houston', 'Phoenix', 'Philadelphia', 'San Antonio', 'Dallas'];
  const COUNTRIES = ['USA', 'Canada', 'UK', 'Germany', 'France', 'Australia', 'Japan', 'Brazil'];

  function generateRows(count: number) {
    const rows = [];
    for (let i = 0; i < count; i++) {
      const first = FIRST_NAMES[i % FIRST_NAMES.length];
      const last = LAST_NAMES[Math.floor(i / FIRST_NAMES.length) % LAST_NAMES.length];
      rows.push({
        id: i + 1,
        name: `${first} ${last}`,
        email: `${first.toLowerCase()}.${last.toLowerCase()}@example.com`,
        department: DEPARTMENTS[i % DEPARTMENTS.length],
        phone: `+1-555-${String(i).padStart(4, '0')}`,
        address: `${100 + i} ${['Main', 'Oak', 'Pine', 'Elm', 'Maple'][i % 5]} St`,
        city: CITIES[i % CITIES.length],
        country: COUNTRIES[i % COUNTRIES.length],
        actions: '...',
      });
    }
    return rows;
  }

  function toPinned(value: string) {
    return value === 'none' ? undefined : value;
  }

  const sampleData = generateRows(1000);
  const grid = queryGrid('tbw-grid', container)!;

  function rebuild(pins: Record<string, string>) {
    grid.gridConfig = {
      columns: [
        { field: 'id', header: 'ID', type: 'number', width: 60, pinned: toPinned(pins.pinId ?? 'left') },
        { field: 'name', header: 'Name', width: 150, pinned: toPinned(pins.pinName ?? 'left') },
        { field: 'email', header: 'Email', width: 220, pinned: toPinned(pins.pinEmail ?? 'none') },
        { field: 'department', header: 'Department', width: 150, pinned: toPinned(pins.pinDepartment ?? 'none') },
        { field: 'phone', header: 'Phone', width: 150, pinned: toPinned(pins.pinPhone ?? 'none') },
        { field: 'address', header: 'Address', width: 250, pinned: toPinned(pins.pinAddress ?? 'none') },
        { field: 'city', header: 'City', width: 120, pinned: toPinned(pins.pinCity ?? 'none') },
        { field: 'country', header: 'Country', width: 120, pinned: toPinned(pins.pinCountry ?? 'none') },
        { field: 'actions', header: 'Actions', width: 100, pinned: toPinned(pins.pinActions ?? 'right') },
      ],
      fitMode: 'fixed',
      features: { pinnedColumns: true },
    };
    grid.rows = sampleData;
  }

  rebuild({});

  container.addEventListener('control-change', ((e: CustomEvent) => {
    rebuild(e.detail.allValues as Record<string, string>);
  }) as EventListener);
}
```

Use the controls below to change which columns are pinned to the left, right, or not pinned.
Pinned columns are automatically reordered to the grid edges—left-pinned columns move to the
start, right-pinned columns move to the end. Unpinning restores the column to its original position.

## Configuration Options

The plugin has no configuration options. It is activated when columns have `pinned: 'left'` or `pinned: 'right'` set.

## Programmatic API

See [`PinnedColumnsPlugin`](./classes/pinnedcolumnsplugin/) for the full list of methods.

```ts
const plugin = grid.getPluginByName('pinnedColumns');

// Get currently pinned columns
const leftPinned = plugin.getLeftPinnedColumns();
const rightPinned = plugin.getRightPinnedColumns();

// Clear all sticky positions
plugin.clearStickyPositions();

// Recalculate offsets (e.g., after column resize)
plugin.refreshStickyOffsets();
```

:::note
To pin or unpin columns at runtime, update the column's `pinned` property in your `gridConfig.columns` and re-assign the config. The plugin reads `pinned` from column definitions during each render cycle.
:::

### RTL Support

For direction-independent pinning, use logical values `'start'` and `'end'` instead of `'left'`/`'right'`:

```ts
{ field: 'id', pinned: 'start' }  // Left in LTR, Right in RTL
{ field: 'actions', pinned: 'end' } // Right in LTR, Left in RTL
```

## Column Reordering Behaviour

When a column is pinned (either via config or the context menu), the plugin **automatically
reorders** it to the appropriate edge of the grid:

- `pinned: 'left'` → moved to the leftmost position (after other left-pinned columns)
- `pinned: 'right'` → moved to the rightmost position (before other right-pinned columns)

When a column is unpinned via the context menu, it is restored to its **original position**
in the column order.

This ensures pinned columns are always visible at the grid edges without requiring the user
to scroll.

## Known Limitations

- **Column Groups support**: Pinned columns work with the
  [Column Groups plugin](../grouping-columns/). When a pinned column belongs to a
  column group, the group header is automatically split so the pinned portion stays
  sticky while the rest scrolls normally. For explicit (named) groups, the group label
  scrolls with the non-pinned fragment until it reaches the pinned edge, then transfers
  to the pinned fragment for a seamless experience.

- **Header-only sticky in virtualized grids**: Due to the row virtualization architecture
  (`will-change: transform` on the rows container), `position: sticky` only works on
  **header cells**. Data row cells receive the sticky CSS classes but the browser cannot
  honour them because the transformed ancestor creates a new containing block.

## See Also

- **[Pinned Rows](/grid/plugins/pinned-rows.md)** — Pin rows to top/bottom
- **[Column Reorder](../reorder-columns/)** — Drag-to-reorder columns
- **[Column Groups](../grouping-columns/)** — Group column headers
