# Master-detail

> Expand a row into a detail panel underneath it, drawn by a snippet with anything inside, including another grid.

Master-detail lets a row expand into a panel underneath it, for information that does not fit in columns: an order's fills, a customer's contacts, a chart. The grid adds a leading expand toggle to each row and draws the panel from a snippet you pass.

Click the toggle at the start of a row to open its panel; open several, then scroll:

Example: Rows that expand into a detail panel (master-detail.svelte)

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

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

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

<!-- Anything can go in the panel: text, a form, another grid. -->
{#snippet detail({ row }: { row: GridRow })}
  <div class="detail">
    <strong>{row.name}</strong> · {row.sector}
    <dl>
      <dt>Last</dt><dd>{Number(row.price).toFixed(2)}</dd>
      <dt>Change</dt><dd>{Number(row.change).toFixed(2)}%</dd>
      <dt>Volume</dt><dd>{Number(row.volume).toLocaleString()}</dd>
    </dl>
  </div>
{/snippet}

<Grid {rows} {columns} {detail} detailHeight={96} height={300} />

<style>
  .detail {
    padding: 12px 16px;
    font-size: 13px;
  }
  dl {
    display: grid;
    grid-template-columns: repeat(3, max-content max-content);
    gap: 4px 12px;
    margin: 8px 0 0;
  }
  dt {
    opacity: 0.6;
  }
  dd {
    margin: 0 16px 0 0;
  }
</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 }));
}
```

## Adding a detail panel

Pass a `detail` snippet, declared inside `<Grid>` so it is typed from the prop. It receives `{ row }`, and its content can be anything, including another `Grid`:

```svelte
<Grid {rows} {columns} detailHeight={120} height={480}>
  {#snippet detail({ row })}
    <div class="detail">
      <strong>{row.name}</strong> · {row.sector}
    </div>
  {/snippet}
</Grid>
```

- `detailHeight` sets the panel's height in pixels (default 160).
- Expanded panels are virtualized with the rows, so many open panels stay cheap to scroll.
- Master-detail needs the rows in memory; it is not available with a server [data source](https://bonguynvan.github.io/bo-grid/docs/server-data/). Rows keep one height, so give `rowHeight` a number rather than a function.
- A detail row breaks [merged-cell](https://bonguynvan.github.io/bo-grid/docs/merged-cells/) runs.

To highlight one "open" row without expanding it, for a list beside a separate detail view, use `selectedRowId` and `onRowClick` instead (see [Row selection](https://bonguynvan.github.io/bo-grid/docs/row-selection/#one-active-row)).

## Outside Svelte

`detail` is a Svelte snippet, so the React and Vue components and the `<bo-grid>` element cannot use it. See [React, Vue & more](https://bonguynvan.github.io/bo-grid/docs/frameworks/#custom-cells-outside-svelte).

## Reference

- `detail`: `Snippet<[{ row: GridRow }]>` — Master-detail: render an expandable detail panel under a row. Adds a leading expand-toggle column. In-memory mode only (overrides rowHeight).

  ```svelte
  <Grid {rows} {columns} height={480}>
    {#snippet detail({ row })}<p>{row.sym}: {row.note}</p>{/snippet}
  </Grid>
  ```

- `detailHeight`: `number` (default `160`) — Height (px) of the expanded detail panel.

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