# Pinned columns

> Keep key columns, such as a symbol or a total, in view on the left or right edge while the rest of the grid scrolls sideways.

A pinned column stays in view while the other columns scroll sideways. Pin the column that identifies a row (a symbol, a name) to the left, and a summary column (a total, an action) to the right.

Scroll the grid sideways: Symbol stays on the left and Volume on the right while the returns in between scroll under them.

Example: Columns pinned left and right (pinned-columns.svelte)

```svelte
<script lang="ts">
  import { Grid, type ColumnDef, type GridRow } from 'bo-grid';
  import { quotes } from './data';

  const pct = (r: GridRow, f: number) => Number(r.change) * f;

  // Fixed widths wider than the frame: the middle scrolls sideways under the
  // pinned Symbol column on the left and the Volume column on the right.
  const columns: ColumnDef[] = [
    { type: 'text', key: 'symbol', header: 'Symbol', width: 90, pinned: 'left' },
    { type: 'text', key: 'name', header: 'Company', width: 150 },
    { type: 'text', key: 'sector', header: 'Sector', width: 130 },
    { type: 'price', key: 'price', header: 'Price', width: 100 },
    { type: 'percent', key: 'change', header: '1 day', width: 100 },
    { type: 'percent', key: 'w1', header: '1 week', width: 100, value: (r) => pct(r, 2.4) },
    { type: 'percent', key: 'm1', header: '1 month', width: 100, value: (r) => pct(r, -3.1) },
    { type: 'percent', key: 'y1', header: '1 year', width: 100, value: (r) => pct(r, 9.6) },
    { type: 'volume', key: 'volume', header: 'Volume', width: 110, pinned: 'right' },
  ];

  let rows = $state(quotes());
</script>

<Grid {rows} {columns} height={300} />
```

data.ts:

```ts
// Small, fixed data for the docs' examples: every example reads the same
// symbols, so a reader compares features, not datasets.
// A type, not an interface: it then fits GridRow's index signature.
export type Quote = {
  id: number;
  symbol: string;
  name: string;
  sector: string;
  price: number;
  change: number;
  volume: number;
};

const BASE: Omit<Quote, 'id'>[] = [
  { symbol: 'VNM', name: 'Vinamilk', sector: 'Consumer', price: 68.5, change: 1.2, volume: 1_204_000 },
  { symbol: 'FPT', name: 'FPT Corp', sector: 'Technology', price: 132, change: -0.4, volume: 860_500 },
  { symbol: 'HPG', name: 'Hoa Phat', sector: 'Materials', price: 27.35, change: 0.74, volume: 3_412_900 },
  { symbol: 'VCB', name: 'Vietcombank', sector: 'Financials', price: 91.5, change: 0.55, volume: 1_986_200 },
  { symbol: 'MWG', name: 'Mobile World', sector: 'Consumer', price: 51.4, change: -1.1, volume: 2_250_400 },
  { symbol: 'SSI', name: 'SSI Securities', sector: 'Financials', price: 33.3, change: -0.45, volume: 4_104_700 },
  { symbol: 'VIC', name: 'Vingroup', sector: 'Real estate', price: 44.15, change: -0.11, volume: 1_530_100 },
  { symbol: 'GAS', name: 'PV Gas', sector: 'Energy', price: 71.8, change: 0.3, volume: 645_800 },
  { symbol: 'MSN', name: 'Masan', sector: 'Consumer', price: 76.2, change: 2.05, volume: 1_120_300 },
  { symbol: 'TCB', name: 'Techcombank', sector: 'Financials', price: 23.9, change: 0.84, volume: 5_870_000 },
  { symbol: 'VHM', name: 'Vinhomes', sector: 'Real estate', price: 41.6, change: -0.72, volume: 2_904_600 },
  { symbol: 'POW', name: 'PV Power', sector: 'Energy', price: 12.25, change: 1.66, volume: 6_315_200 },
];

/** A fresh copy of the quotes, so one example's edits never reach another. */
export function quotes(): Quote[] {
  return BASE.map((q, i) => ({ id: i + 1, ...q }));
}
```

## Pinning a column

Set `pinned` on the column: `true` or `'left'` pins it to the left edge, `'right'` to the right edge.

```ts
const columns: ColumnDef[] = [
  { type: 'text', key: 'symbol', header: 'Symbol', width: 90, pinned: 'left' },
  { type: 'price', key: 'price', header: 'Price', width: 100 },
  // …more fixed-width columns scroll under the pinned ones…
  { type: 'volume', key: 'volume', header: 'Volume', width: 110, pinned: 'right' },
];
```

- Pinning is opt-in. With no pinned columns the grid fits its columns to its width and never scrolls sideways unless they don't fit.
- Give the scrolling columns fixed widths, so there is something to scroll.
- Users can pin and unpin from the [column menu](https://bonguynvan.github.io/bo-grid/docs/column-moving/#the-column-menu) (`columnMenu`). Their pins are part of `api.getState()` and are remembered under `persistKey`.

## With wide grids

For grids with dozens or hundreds of columns, `virtualizeColumns` renders only the columns in the horizontal window. Pinned columns always render. See [Wide grids](https://bonguynvan.github.io/bo-grid/docs/wide-grids/).

## Pinning rows

To keep rows in view instead, such as a watchlist's favourites, see `pinnedRows` and `rowPinning` in [Cell selection & clipboard](https://bonguynvan.github.io/bo-grid/docs/selection/).

## Reference

- `pinned`: `boolean | 'left' | 'right'` (default `false`) — Pin this column so it stays visible during horizontal scroll. `true` / `'left'` pins to the left edge, `'right'` to the right.
- `width`: `number` (default `96`) — Width in px. A `flex` column starts from it where columns have fixed widths (pinned columns, `virtualizeColumns`), and `fitColumns` scales columns in proportion to it.

- `columnMenu`: `boolean` (default `false`) — Enable a per-column header menu (a ⋮ trigger) with sort, hide and (with `filterMenu`) filter actions.
- `virtualizeColumns`: `boolean` (default `false`) — Render only the columns within the horizontal scroll window (+ overscan) for very wide grids (100+ columns). Forces fixed-width horizontal scroll; pinned columns always render. Off, the columns fit the grid's width.
- `persistKey`: `string` (default `undefined`) — Remember the user's layout (column order, widths, hidden and pinned columns, pinned rows) in localStorage under this key.

---
Page: https://bonguynvan.github.io/bo-grid/docs/pinned-columns/ · All pages: https://bonguynvan.github.io/bo-grid/llms.txt
