# Header groups

> Put a spanning parent header over related columns, such as Bid, Ask and Match on a price board.

A header group is a second header row that spans related columns, so a reader sees which columns belong together: Bid price and volume, Ask price and volume, the match price and change. Columns in a group keep their own headers, sorting and resizing.

Bid, Ask and Match each span the columns under them; sort or resize any column as usual:

Example: Bid, Ask and Match groups (header-groups.svelte)

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

  const bid = (r: GridRow) => Number(r.price) - 0.05;
  const ask = (r: GridRow) => Number(r.price) + 0.05;

  // Consecutive columns with the same `group` sit under one spanning header.
  const columns: ColumnDef[] = [
    { type: 'text', key: 'symbol', header: 'Symbol', width: 90 },
    { type: 'price', key: 'bid', header: 'Price', width: 90, group: 'Bid', value: bid },
    { type: 'volume', key: 'bidVol', header: 'Vol', width: 90, group: 'Bid', value: (r) => Math.round(Number(r.volume) * 0.04) },
    { type: 'price', key: 'ask', header: 'Price', width: 90, group: 'Ask', value: ask },
    { type: 'volume', key: 'askVol', header: 'Vol', width: 90, group: 'Ask', value: (r) => Math.round(Number(r.volume) * 0.03) },
    { type: 'price', key: 'price', header: 'Last', width: 90, group: 'Match' },
    { type: 'percent', key: 'change', header: 'Chg %', width: 90, group: 'Match' },
    { type: 'volume', key: 'volume', header: 'Vol', flex: 1, group: 'Match' },
  ];

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

<Grid {rows} {columns} fitColumns 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 }));
}
```

## Grouping columns

Give columns a `group` label. Consecutive columns with the same label render under one spanning header:

```ts
const columns: ColumnDef[] = [
  { type: 'text', key: 'symbol', header: 'Symbol', width: 90 },
  { type: 'price', key: 'bid', header: 'Price', width: 90, group: 'Bid' },
  { type: 'volume', key: 'bidVol', header: 'Vol', width: 90, group: 'Bid' },
  { type: 'price', key: 'ask', header: 'Price', width: 90, group: 'Ask' },
  { type: 'volume', key: 'askVol', header: 'Vol', width: 90, group: 'Ask' },
];
```

- Only consecutive columns join a group. The same label further along starts a new span, so if users can move columns, a group can split in two.
- Columns without a `group` have an empty cell above them.
- A group keeps the width of its columns as they resize, flex columns included.

## Reference

- `group`: `string` (default `undefined`) — Parent header label. Consecutive columns sharing a `group` render under a spanning header, which keeps the width of its columns (flex ones too).

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