# Pinned rows

> Keep rows above the scroll, always in view, whether they are rows of your own or rows readers pin from the board.

A pinned row stays above the scroll while the rest of the grid moves under it. Use it for a row that should never leave the screen: a benchmark, a summary, your position, or the symbols a reader is watching on a long board.

The INDEX row is passed in `pinnedRows`; pin others yourself with a right-click, or with the button, which calls the grid API.

Example: Pinned rows (pinned-rows.svelte)

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

  const columns: ColumnDef[] = [
    { type: 'text', key: 'symbol', header: 'Symbol', width: 110 },
    { type: 'text', key: 'name', header: 'Company', width: 150 },
    { type: 'price', key: 'price', header: 'Price', width: 96 },
    { type: 'percent', key: 'change', header: 'Chg %', width: 96 },
    { type: 'text', key: 'sector', header: 'Sector', flex: 1 },
  ];

  let rows = $state(quotes());
  // A row of your own, always on top: its id (0) must not clash with the rows' (1–12).
  const benchmark = { id: 0, symbol: 'INDEX', name: 'Market index', price: 1288.4, change: 0.42, sector: '—' };

  let api: GridApi | undefined;
  let pinned = $state<(string | number)[]>([]);
</script>

<p class="bar">
  Right-click a row to pin it.
  <button onclick={() => api?.pinRow(2, !pinned.includes(2))}>{pinned.includes(2) ? 'Unpin' : 'Pin'} FPT</button>
  <span>Pinned: {pinned.length ? pinned.join(', ') : 'none'}</span>
</p>
<Grid {rows} {columns} height={300} pinnedRows={[benchmark]} rowPinning onRowPinChange={(ids) => (pinned = ids)} onReady={(a) => (api = a)} />

<style>
  .bar {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: 6px 14px;
    margin: 0 0 8px;
    font-size: 12px;
  }
</style>
```

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 }));
}
```

## Rows of your own

`pinnedRows` takes rows that are not in `rows`: they sit above the scroll, use the grid's columns and `rowClass`, and are display-only. Their ids must not clash with ids in `rows`.

```svelte
<Grid {rows} {columns} height={480} pinnedRows={[benchmark]} />
```

## Rows readers pin

`rowPinning` lets readers pin rows themselves. Right-clicking a row offers **Pin to top**, and **Unpin** on a pinned row, ahead of any `rowMenu` items.

- A pinned row shows above the scroll and also stays in its place in the body. It keeps updating, `api.patchRows` included.
- Pins are remembered with `persistKey`, and carried by `getState()` and `applyState()`.
- `onRowPinChange(ids)` reports every change.
- Pinning works on rows in `rows`; with a server `source`, pin rows of your own with `pinnedRows`.

```svelte
<Grid {rows} {columns} height={480} rowPinning persistKey="watchlist" onRowPinChange={(ids) => save(ids)} />
```

## From code

`api.pinRow(id)` pins a row and `api.pinRow(id, false)` unpins it; it returns `false` when no row has that id. `api.getPinnedRowIds()` returns the pinned ids in the order they were pinned.

```ts
api.pinRow(2);           // pin the row with id 2
api.pinRow(2, false);    // unpin it
api.getPinnedRowIds();   // [] again
```

## Reference

- `pinnedRows`: `GridRow[]` (default `[]`) — Rows pinned to the top, always visible above the scroll (a benchmark, a summary, "your position"). Display-only — not virtualized or selectable. Their `getRowId` must not collide with a row in `rows`/`source` — with a derived-flash column (`flash: 'auto'`), a shared id means the pinned cell and the data-row cell share one flash-tracker entry, so updating one can suppress or trigger a flash on the other. To let users pin rows of `rows` themselves, use `rowPinning`.

  ```svelte
  <Grid {rows} {columns} height={480} pinnedRows={[{ id: -1, sym: 'VN30', last: index }]} />
  ```

- `rowPinning`: `boolean` (default `false`) — Let users pin rows above the scroll from the row menu (right-click): "Pin to top" / "Unpin". A pinned row stays in the body too, keeps updating, and is remembered with `persistKey` and in `getState()`.
- `onRowPinChange`: `(ids: (string | number)[]) => void` — Called with the pinned row ids (in pin order) whenever they change.

  ```svelte
  <Grid {rows} {columns} height={480} rowPinning onRowPinChange={(ids) => save('pinned', ids)} />
  ```

- `persistKey`: `string` (default `undefined`) — Remember the user's layout (column order, widths, hidden and pinned columns, pinned rows) in localStorage under this key.

- `pinRow(key: string | number, pinned?: boolean): boolean` — Pin a row above the scroll (or unpin it with `pinned: false`), as the row menu does under `rowPinning`; `pinned` is true unless given. The row stays in the body too. Returns false when no row has that id.

  ```ts
  api.pinRow(row.id);
  ```

- `getPinnedRowIds(): (string | number)[]` — Ids of the rows pinned at runtime, in pin order.

  ```ts
  for (const id of api.getPinnedRowIds()) api.pinRow(id, false);
  ```

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