# Cell flashing

> Flash a cell green or red when its value moves, show how far it moved, flash cells on demand, and keep a busy board's flashes cheap.

A tick flash tells the eye which values just moved. Set `flash: 'auto'` on a column and the grid does the rest: it remembers each cell's last value and flashes green on a rise, red on a fall. There is nothing to keep in app code; write the new value and the cell flashes.

Bid and ask below flash on their own, often in opposite directions in the same row. Status flashes amber whenever it changes. The buttons flash cells whose values did not change, and Motion switches between a fading and a held flash.

Example: Flashing cells (flashing.svelte)

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

  // Bid and ask flash by direction, each cell on its own; Status flashes
  // amber on any change. The rows are $state, so a write is all it takes.
  const STATUSES = ['Open', 'Halted', 'Auction'];
  const rows = $state(quotes().slice(0, 8).map((q) => ({ ...q, bid: q.price - 0.05, ask: q.price + 0.05, status: 'Open' })));
  const columns: ColumnDef[] = [
    { type: 'text', key: 'symbol', header: 'Symbol', width: 90 },
    { type: 'price', key: 'bid', header: 'Bid', flash: 'auto' },
    { type: 'price', key: 'ask', header: 'Ask', flash: 'auto' },
    { type: 'text', key: 'status', header: 'Status', flash: 'change', flashMs: 900 },
    { type: 'text', key: 'name', header: 'Name', flex: 1 },
  ];

  let motion = $state<'fade' | 'hold'>('fade');
  let api: GridApi | undefined;

  $effect(() => {
    const timer = setInterval(() => {
      const r = rows[Math.floor(Math.random() * rows.length)];
      const move = Math.round((Math.random() - 0.5) * 4) * 0.05;
      r.bid = Math.round((r.bid + move) * 100) / 100;
      r.ask = Math.round((r.ask + move + (Math.random() < 0.3 ? 0.05 : 0)) * 100) / 100;
      if (Math.random() < 0.05) r.status = STATUSES[Math.floor(Math.random() * STATUSES.length)];
    }, 120);
    return () => clearInterval(timer);
  });
</script>

<div class="bar">
  <button onclick={() => api?.flashCells({ rows: [rows[0].id] })}>Flash VNM's row</button>
  <button onclick={() => api?.flashCells({ rows: rows.map((r) => r.id), columns: ['symbol'], dir: 'up', ms: 1200 })}>Flash every symbol as a rise</button>
  <label>Motion <select bind:value={motion}><option>fade</option><option>hold</option></select></label>
</div>
<Grid {rows} {columns} height={300} flashMotion={motion} onReady={(a) => (api = a)} />

<style>
  .bar {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: 8px;
    margin: 0 0 8px;
    font-size: 12px;
  }
  button,
  select {
    font: inherit;
  }
</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 }));
}
```

## Flash modes

```ts
const columns: ColumnDef[] = [
  { type: 'price', key: 'bid', header: 'Bid', flash: 'auto' },
  { type: 'text', key: 'status', header: 'Status', flash: 'change', flashMs: 900 },
];
row.bid = next; // the bid cell flashes, in the direction it moved
```

| `flash` | What flashes |
| --- | --- |
| `'auto'` (alias `'up-down'`) | Each cell on its own: green when the number rose, red when it fell. For prices and other numbers. |
| `'change'` | Each cell on its own, in a neutral amber, on any change: status, quantity, text. |
| `true` | Legacy row-driven mode: every `flash: true` column in the row flashes together, following the row's own `flashSeq` and `flashDir`, which you bump yourself. |

The derived modes are per cell: bid can flash green while ask flashes red in the same row on the same frame. Flash state is keyed on the row id and the column, so a cell stays quiet on first paint and when it scrolls back into view; only a real change flashes.

`flashMs` sets the duration (300 ms by default). Raise it for a slow feed, lower it for a fast one so flashes don't smear together. A column that colours its own text (price-limit tones, for example) should set `flashColor: false`, so the flash lights only the background.

Under `prefers-reduced-motion: reduce` the grid plays no flash at all.

## Showing the change

`showChange: true` shows how far a value moved, not only which way: `▲0.15` beside the price in the up colour (`▼` down), held, then faded over a second.

```ts
{ type: 'price', key: 'last', header: 'Last', flash: 'auto', showChange: true }
{ type: 'price', key: 'last', header: 'Last', showChange: { format: (d) => (d > 0 ? '+' : '') + d.toFixed(2), ms: 1500 } }
```

- The delta is written in the column's own format; `format(delta, row)` receives the signed change, and `ms` sets how long it shows (1,000 by default).
- In a narrow column the delta gives way first, so the value is never cut for it.
- It fades by animating the text colour, so it adds no compositor layers.

Use it on the one or two columns that matter, such as the match price. Each change runs a short animation, so a busy board with it on every column pays real frame time.

## Flash on demand

Some events don't change a value on screen: a block trade, your order filling, an alert level crossed. `api.flashCells` plays the tick flash on the cells you name, changed or not:

```ts
api.flashCells({ rows: [fill.symbol] });                       // the whole row, amber
api.flashCells({ rows: ids, columns: ['symbol'], ms: 1200 }); // just the symbol, longer
api.flashCells({ rows: [id], columns: ['last'], dir: 'up' });  // tinted like a rise
```

- `rows` are row ids (every rendered row when omitted), and `columns` are column keys (every column when omitted).
- `dir: 'up' | 'down'` tints like a rise or a fall; `ms` overrides each column's `flashMs`.
- Only rows on screen flash, as with a tick. The call returns how many rows it flashed.
- Value cells flash (numbers and text). Cells that draw their own content (badges, sparklines, components) don't.
- A tick that lands during the flash takes over with its own direction, and a repeat call replays it.

## Busy boards

By default a flash fades out. A CSS fade is restyled and repainted on every frame it runs, and on a busy board most visible cells are always mid-fade, which becomes most of the frame. `flashMotion="hold"` shows a static tint for the flash window instead: repainted when it starts and when the grid's clock drops it, while a repeat tick during the hold changes nothing.

```svelte
<Grid {rows} {columns} flashMotion="hold" onReady={(a) => (api = a)} height={480} />
```

Measured on the Price board with motion allowed, held flashes cut the main thread's work per frame from about 30 to about 9.4 ms at 10,000 events/s, and from about 40 to about 12.3 ms at 30,000, with no dropped frames. That is within about 1 ms of no flash at all ([BENCHMARKS.md](https://github.com/bonguynvan/bo-grid/blob/main/BENCHMARKS.md#live-frames-where-the-time-goes)).

For a grid that is sometimes quiet and sometimes busy, `flashMotion="auto"` decides per flash as it starts. It fades while few cells flash at once and holds once more than about 120 overlap. A taller board reaches that sooner, which is the point: the cost grows with the fading cells on screen. A flash that lights whole rows (`flashNewRows`, `api.flashCells`) counts every cell it lights, so a burst of new orders on a blotter holds too.

The trading desk board below flashes prices, changes and volume across 1,000 rows, with sparklines and heat cells:

Live example: https://bonguynvan.github.io/bo-grid/examples/trading/ (source: https://github.com/bonguynvan/bo-grid/blob/main/src/demo/examples/)

## Reference

- `flash`: `boolean | 'auto' | 'up-down' | 'change'` (default `false`) — Flash the cell when its value changes. - `'auto'` (alias `'up-down'`) — the grid derives it: green on a rise, red on a fall, per cell. Nothing to maintain in app code; just stream new values in. This is what you want for a live price board. - `'change'` — a neutral flash on any change, numeric or not (status, qty). - `true` — legacy row-driven mode: the flash follows the row's own `flashSeq`/`flashDir`, so every `flash: true` column in a row flashes together, in one direction, and you bump the counter yourself. Derived modes never flash on first paint or when a row scrolls into view.
- `flashMs`: `number` (default `300`) — Flash duration in ms — **derived modes only** (`'auto'`/`'up-down'`/ `'change'`); has no effect on legacy `flash: true`, which flashes for the CSS animation's fixed duration regardless. Raise it for a slow feed, lower it for a fast one so flashes don't smear together.
- `flashColor`: `boolean` (default `true`) — Tint the text in the tick's direction while flashing. Set false when the column colours its own text — e.g. price-limit tones on a VN board (ceiling / floor / reference) — so the flash only lights the background.
- `showChange`: `boolean | ShowChangeOptions` (default `false`) — Show the size of each change beside the value for a moment — `▲0.15` in the up colour, `▼` down — held, then faded. The delta is the last change, written in the column's own format. Numeric values only; first paint and rows scrolling into view show nothing. `{ format, ms }` sets the text (`format(delta, row)` gets the signed change) and how long it shows (1000 ms unless set). In a narrow column the delta gives way first: the value itself is never cut for it. Meant for one or two key columns (the match price): each change runs a short animation, so a busy board with it on every column pays real frame time.

  ```ts
  { type: 'price', key: 'last', header: 'Last', flash: 'auto', showChange: { ms: 1500, format: (d) => `${d > 0 ? '+' : ''}${d.toFixed(2)}` } }
  ```


- `flashMotion`: `'fade' | 'hold' | 'auto'` (default `'fade'`) — How a flash plays. `'fade'`: the tint fades out. `'hold'`: the tint holds for the flash window, then drops — no CSS animation, so a busy board restyles and repaints a flashing cell twice instead of every frame. `'auto'`: fade while few cells flash at once, hold when many do. A flash that lights whole rows (`flashNewRows`, `api.flashCells`) counts each cell it lights.
- `flashNewRows`: `boolean | number` (default `false`) — Flash a row once when it arrives in `rows` (a new order, a new fill): every cell, in the neutral flash colour, for this many ms (`true`: 600). A row arrives when its id is new; a copy of a row already there does not flash, nor do the rows an empty grid gets (a load or a reload), nor a wholesale replacement (more than 64 rows, and more than an eighth of them, at once). Rows on screen only, as with every flash.

  ```svelte
  <Grid {rows} {columns} height={480} flashNewRows={900} />
  ```


- `flashCells(opts?: FlashCellsOptions): number` — Flash cells on demand — a fill, an alert, a row to notice — with the tick animation, whether or not their values changed. Only rows on screen flash. Value cells flash (numbers, text); cells that draw their own content (badges, sparklines, components) do not. Returns how many rows it flashed.

  ```ts
  api.flashCells({ rows: [fill.orderId], dir: fill.side === 'buy' ? 'up' : 'down', ms: 800 });
  ```

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