Highlighting changes
Built-in cell flash (cellFlash)
Set cellFlash on a column and the grid flashes that cell whenever its value
changes - edits, streaming feeds, server pushes. It is keyed by row identity,
so scrolling a virtualized grid (which recycles <td> nodes) never triggers a
false flash; only a same-row value change does. It respects
prefers-reduced-motion.
{ field: 'price', cellFlash: true } // default tint
{ field: 'change', cellFlash: { className: 'flash-up-down' } } // your own animation
The default flash fades a theme-tinted background (--sg-cell-flash, or the
accent). For directional colouring (green up / red down) pass your own class
and toggle it from a cellClass callback, or animate flash-up-down in CSS.
Open the live example: Stock market - live (Real-time & Streaming)
Rolling your own
If you need more than a flash (persistent dirty markers, diff badges), you can still build it by hand with a diff against a frozen snapshot of the data.
Dirty cells while editing
Demo 5 (demos/05-inline-editing.svelte) does this:
<script lang="ts">
// svelte-ignore state_referenced_locally
let initial = rows.map((r) => ({ ...r }))
let dirty = $state<Record<string, true>>({})
$effect(() => {
if (!api) return
const snap = api.getData()
const next: Record<string, true> = {}
for (let i = 0; i < snap.length; i++) {
const a = snap[i]!
const b = initial[i]
if (!b) continue
for (const key of Object.keys(a)) {
if ((a as any)[key] !== (b as any)[key]) {
next[`${a.id}.${key}`] = true
}
}
}
dirty = next
})
</script>
To make the dirty marker visible in the grid, render an indicator inside a
custom cell:
{
field: 'salary',
cell: (ctx) => renderSnippet(MaybeDirty, {
value: ctx.getValue(),
isDirty: dirty[`${ctx.row.original.id}.salary`] === true,
}),
}
{#snippet MaybeDirty(p: { value: unknown; isDirty: boolean })}
<span class="inline-flex items-center gap-1">
{p.value}
{#if p.isDirty}<span class="h-1.5 w-1.5 rounded-full bg-amber-500"></span>{/if}
</span>
{/snippet}
Flash on value change (live data)
For live-update grids (stock tickers, queue dashboards):
<script lang="ts">
let lastValues = new Map<string, unknown>()
let flashing = $state<Record<string, true>>({})
function onCellSeen(key: string, value: unknown) {
if (lastValues.has(key) && lastValues.get(key) !== value) {
flashing[key] = true
setTimeout(() => { delete flashing[key] }, 500)
}
lastValues.set(key, value)
}
</script>
Drive the flash from your cell renderer the same way. Use prefers- reduced-motion to disable the animation for users who opt out.
Flash on change
cellFlash tints a cell for a moment whenever its value changes. On a live
feed that is the difference between a number that updated and one you happened
to be looking at.
<script lang="ts">
import { SvGrid, renderSnippet, type GridColumns } from '@svgrid/grid'
type Person = {
id: number
name: string
department: string
city: string
age: number
salary: number
joined: string
active: boolean
}
const people: Person[] = [
{ id: 1, name: 'Ada Lovelace', department: 'Engineering', city: 'London', age: 36, salary: 142000, joined: '2021-03-01', active: true },
{ id: 2, name: 'Grace Hopper', department: 'Engineering', city: 'New York', age: 45, salary: 168000, joined: '2019-07-15', active: true },
{ id: 3, name: 'Linus Torvalds', department: 'Platform', city: 'Portland', age: 54, salary: 155000, joined: '2020-01-20', active: false },
{ id: 4, name: 'Radia Perlman', department: 'Networking', city: 'Seattle', age: 49, salary: 161000, joined: '2022-09-05', active: true },
]
let rows = $state<Person[]>(people.map((p) => ({ ...p })))
function tick() {
const i = Math.floor(Math.random() * rows.length)
rows[i] = { ...rows[i]!, salary: rows[i]!.salary + Math.round((Math.random() - 0.4) * 4000) }
}
const columns: GridColumns<Person> = [
{ field: 'name', header: 'Name', width: 190 },
{ field: 'salary', header: 'Salary', width: 140, cellFlash: true,
format: { type: 'currency', currency: 'USD' } },
]
</script>
<button type="button" onclick={tick}>Change one salary</button>
<SvGrid data={rows} {columns} />
Your own flash class
Pass { className } to style the flash yourself - a green-up / red-down pair is
the usual next request, and it is CSS rather than configuration.
<script lang="ts">
import { SvGrid, renderSnippet, type GridColumns } from '@svgrid/grid'
type Person = {
id: number
name: string
department: string
city: string
age: number
salary: number
joined: string
active: boolean
}
const people: Person[] = [
{ id: 1, name: 'Ada Lovelace', department: 'Engineering', city: 'London', age: 36, salary: 142000, joined: '2021-03-01', active: true },
{ id: 2, name: 'Grace Hopper', department: 'Engineering', city: 'New York', age: 45, salary: 168000, joined: '2019-07-15', active: true },
{ id: 3, name: 'Linus Torvalds', department: 'Platform', city: 'Portland', age: 54, salary: 155000, joined: '2020-01-20', active: false },
{ id: 4, name: 'Radia Perlman', department: 'Networking', city: 'Seattle', age: 49, salary: 161000, joined: '2022-09-05', active: true },
]
let rows = $state<Person[]>(people.map((p) => ({ ...p })))
const columns: GridColumns<Person> = [
{ field: 'name', header: 'Name', width: 190 },
{ field: 'salary', header: 'Salary', width: 140,
cellFlash: { className: 'bump' },
format: { type: 'currency', currency: 'USD' } },
]
</script>
<button type="button" onclick={() => (rows[0] = { ...rows[0]!, salary: rows[0]!.salary + 1000 })}>
Raise the first
</button>
<SvGrid data={rows} {columns} />
<style>
:global(.bump) { animation: bump 700ms ease-out; }
@keyframes bump {
from { background: color-mix(in srgb, #22c55e 35%, transparent); }
to { background: transparent; }
}
</style>
See also
Live examples
- Stock market - live - WebSocket-style ticking feed. Cells flash on up/down ticks, pause control, throttle.
- Inline editing - Typed editors (text/number/checkbox/date) with dirty tracking + save.