# Row styles

> Style rows and cells from their data with classes, react to a row being opened, and give rows a right-click menu.

Rows and cells take classes chosen from their data: `rowClass` for a whole row, a column's `cellClass` for its cells. The same page covers what a row does when used: `onRowClick` when it is opened, and `rowMenu` for its right-click menu.

Falling stocks are dimmed by `rowClass`, and big gains are bold through `cellClass`. Click a row, or right-click it for its menu:

Example: Row classes, row click and a row menu (row-styles.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: 150 },
    { type: 'price', key: 'price', header: 'Price', width: 96 },
    // A class on this column's cells, chosen by value.
    { type: 'percent', key: 'change', header: 'Chg %', width: 96, cellClass: (v) => (Number(v) >= 1 ? 'hot' : undefined) },
    { type: 'volume', key: 'volume', header: 'Volume', flex: 1 },
  ];

  let rows = $state(quotes());
  let last = $state('');

  // A class on the whole row, chosen by its data.
  const rowClass = (r: GridRow) => (Number(r.change) < 0 ? 'falling' : undefined);
  const rowMenu = (r: GridRow) => [
    { label: `Copy ${r.symbol}`, onSelect: () => navigator.clipboard?.writeText(String(r.symbol)) },
    { label: 'Remove row', onSelect: () => (rows = rows.filter((x) => x.id !== r.id)) },
  ];
</script>

<p class="state">Click a row or press Enter on it; right-click for its menu. Last opened: {last || 'none'}</p>
<Grid {rows} {columns} {rowClass} {rowMenu} onRowClick={(r) => (last = String(r.symbol))} height={260} />

<style>
  .state {
    margin: 0 0 10px;
    font-size: 12.5px;
    opacity: 0.8;
  }
  :global(.bo-grid .row.falling) {
    opacity: 0.6;
  }
  :global(.bo-grid .c.hot) {
    font-weight: 700;
  }
</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 }));
}
```

## Classes from the data

`rowClass(row)` returns a class for a data row, or nothing. Rows render inside the grid, so target the class with `:global(...)`:

```svelte
<Grid {rows} {columns} rowClass={(r) => (Number(r.change) < 0 ? 'falling' : undefined)} height={480} />

<style>
  :global(.bo-grid .row.falling) {
    opacity: 0.6;
  }
</style>
```

For one column, `cellClass` is a class string, or a function of the cell's value and row; `headerClass` styles the column's header:

```ts
{ type: 'number', key: 'pnl', header: 'P&L', cellClass: (v) => (Number(v) < 0 ? 'loss' : 'gain'), headerClass: 'num-head' }
```

```css
:global(.bo-grid .c.loss) { color: var(--bo-down); }
```

For colour cues that scale with the value, such as bars and heat tints, see [Conditional formatting](https://bonguynvan.github.io/bo-grid/docs/conditional-formatting/).

## Opening a row

`onRowClick(row, event)` fires when a row is clicked, or when <kbd>Enter</kbd> is pressed on its focused cell. Clicks in an edit input or a checkbox don't count. Use it to open a detail view or navigate; to keep the opened row highlighted, pass its id as `selectedRowId` (see [Row selection](https://bonguynvan.github.io/bo-grid/docs/row-selection/#one-active-row)).

## A row menu

`rowMenu(row)` returns the items of a right-click menu for that row; return an empty array for no menu. Each item has a `label` and an `onSelect`, and the menu closes after an item runs, on a click outside, or on <kbd>Esc</kbd>. From the keyboard, the <kbd>ContextMenu</kbd> key or <kbd>Shift</kbd>+<kbd>F10</kbd> opens it at the focused cell.

```svelte
<Grid {rows} {columns} rowMenu={(r) => [{ label: 'Delete', onSelect: () => remove(r.id) }]} height={480} />
```

## Reference

- `rowClass`: `(row: GridRow) => string | undefined` — Return extra CSS class(es) for a data row (e.g. to colour by value). Style them via `:global(.your-class)` since rows live inside the grid.

  ```svelte
  <Grid {rows} {columns} height={480} rowClass={(row) => (row.status === 'Rejected' ? 'rejected' : undefined)} />
  ```

- `onRowClick`: `(row: GridRow, event: MouseEvent | KeyboardEvent) => void` — Called when a data row is activated by click or Enter (open a detail view, navigate, …). Edit-input and checkbox clicks are excluded.

  ```svelte
  <Grid {rows} {columns} height={480} {selectedRowId} onRowClick={(row) => (selectedRowId = row.id)} />
  ```

- `selectedRowId`: `string | number | null` (default `null`) — Controlled "active row" highlight (keyed by `getRowId`) — for master-detail / list-detail where one row is selected. Independent of the checkbox `rowSelection` and the cell range selection. `null` = none.
- `rowMenu`: `(row: GridRow) => Array<{ label: string; onSelect: () => void }>` — Right-click row menu. Return the items for a row; an empty array shows no menu. Each item runs `onSelect` and closes the menu.

  ```svelte
  <Grid {rows} {columns} height={480}
    rowMenu={(row) => [{ label: 'Cancel order', onSelect: () => cancel(row) }, { label: 'Copy id', onSelect: () => copy(String(row.id)) }]} />
  ```


- `cellClass`: `string | ((value: unknown, row: GridRow) => string | undefined)` (default `undefined`) — Extra class(es) for this column's data cells — a static string, or a function of the cell value/row for conditional styling. Target via `:global(.bo-grid .c.your-class)`.

  ```ts
  { type: 'price', key: 'last', header: 'Last', cellClass: (v, row) => (Number(v) >= Number(row.ceiling) ? 'at-ceiling' : undefined) }
  ```

- `headerClass`: `string` (default `undefined`) — Extra class(es) for this column's header. Target via `:global`.

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