Skip to content
weftGitHub

ScrollGrid

A huge read-only grid that scrolls both ways: thousands of rows and columns, and only what you see exists.

SourcePackage @weft-js/warpPlayground

Preview

tsx
import { ScrollGrid } from '@weft-js/warp'export function Latency() {  return (    <ScrollGrid label="Latency by hour, in milliseconds" corner="Route" rows={2000} columns={HOURS} rowHeader={r => `${PROJECTS[r % 10]!.name}/${r}`} cell={(r, c) => latency(r, Number(c.id))} height="400px" />  )}type Status = 'Live' | 'Building' | 'Failed' | 'Paused'const PROJECTS: { id: string; name: string; owner: string; region: string; status: Status; deploys: number; uptime: number; latency: number; cost: number; updated: string }[] = [  { id: 'storefront', name: 'storefront', owner: 'Priya Raman', region: 'eu-west', status: 'Live', deploys: 412, uptime: 99.98, latency: 84, cost: 1240, updated: '2026-10-31' },  { id: 'checkout', name: 'checkout', owner: 'Tomás Oliveira', region: 'eu-west', status: 'Live', deploys: 288, uptime: 99.95, latency: 112, cost: 980, updated: '2026-10-31' },  { id: 'search-api', name: 'search-api', owner: 'Lena Fischer', region: 'us-east', status: 'Building', deploys: 196, uptime: 99.91, latency: 146, cost: 1610, updated: '2026-10-30' },  { id: 'billing', name: 'billing', owner: 'Mara Lindqvist', region: 'us-east', status: 'Live', deploys: 154, uptime: 99.99, latency: 73, cost: 720, updated: '2026-10-29' },  { id: 'mobile-bff', name: 'mobile-bff', owner: 'Sam Okafor', region: 'ap-south', status: 'Failed', deploys: 233, uptime: 99.42, latency: 208, cost: 860, updated: '2026-10-29' },  { id: 'notifier', name: 'notifier', owner: 'Jun Tanaka', region: 'eu-north', status: 'Live', deploys: 97, uptime: 99.97, latency: 58, cost: 310, updated: '2026-10-27' },  { id: 'reports', name: 'reports', owner: 'Noor Haddad', region: 'us-west', status: 'Paused', deploys: 41, uptime: 99.8, latency: 321, cost: 190, updated: '2026-10-22' },  { id: 'edge-cache', name: 'edge-cache', owner: 'Jun Tanaka', region: 'global', status: 'Live', deploys: 318, uptime: 100, latency: 21, cost: 1430, updated: '2026-10-31' },  { id: 'auth', name: 'auth', owner: 'Mara Lindqvist', region: 'eu-west', status: 'Live', deploys: 176, uptime: 99.99, latency: 66, cost: 540, updated: '2026-10-28' },  { id: 'ingest', name: 'ingest', owner: 'Lena Fischer', region: 'us-east', status: 'Live', deploys: 264, uptime: 99.93, latency: 131, cost: 2120, updated: '2026-10-30' },]const HOURS = Array.from({ length: 24 }, (_, h) => ({ id: String(h), header: `${String(h).padStart(2, '0')}:00` }))const latency = (r: number, h: number) => 40 + ((r + 3) * 37 + h * h * 11) % 260

Installation

npm install @weft-js/warp

Usage

tsx
import { ScrollGrid } from '@weft-js/warp'<ScrollGrid label="Latency by hour, in milliseconds" corner="Route" rows={2000} columns={HOURS} rowHeader={r => `${PROJECTS[r % 10]!.name}/${r}`} cell={(r, c) => latency(r, Number(c.id))} height="400px" />

Flagging values

negative marks a cell in the critical colour.

tsx
import { ScrollGrid } from '@weft-js/warp'export function Negative() {  return (    <ScrollGrid label="Change in spend by month" corner="Project" rows={500} columns={MONTH_COLUMNS} rowHeader={r => `${PROJECTS[r % 10]!.name}-${r}`} cell={(r, c) => showSwing(swing(r, Number(c.id)))} negative={(r, c) => swing(r, Number(c.id)) < 0} height="320px" />  )}type Status = 'Live' | 'Building' | 'Failed' | 'Paused'const PROJECTS: { id: string; name: string; owner: string; region: string; status: Status; deploys: number; uptime: number; latency: number; cost: number; updated: string }[] = [  { id: 'storefront', name: 'storefront', owner: 'Priya Raman', region: 'eu-west', status: 'Live', deploys: 412, uptime: 99.98, latency: 84, cost: 1240, updated: '2026-10-31' },  { id: 'checkout', name: 'checkout', owner: 'Tomás Oliveira', region: 'eu-west', status: 'Live', deploys: 288, uptime: 99.95, latency: 112, cost: 980, updated: '2026-10-31' },  { id: 'search-api', name: 'search-api', owner: 'Lena Fischer', region: 'us-east', status: 'Building', deploys: 196, uptime: 99.91, latency: 146, cost: 1610, updated: '2026-10-30' },  { id: 'billing', name: 'billing', owner: 'Mara Lindqvist', region: 'us-east', status: 'Live', deploys: 154, uptime: 99.99, latency: 73, cost: 720, updated: '2026-10-29' },  { id: 'mobile-bff', name: 'mobile-bff', owner: 'Sam Okafor', region: 'ap-south', status: 'Failed', deploys: 233, uptime: 99.42, latency: 208, cost: 860, updated: '2026-10-29' },  { id: 'notifier', name: 'notifier', owner: 'Jun Tanaka', region: 'eu-north', status: 'Live', deploys: 97, uptime: 99.97, latency: 58, cost: 310, updated: '2026-10-27' },  { id: 'reports', name: 'reports', owner: 'Noor Haddad', region: 'us-west', status: 'Paused', deploys: 41, uptime: 99.8, latency: 321, cost: 190, updated: '2026-10-22' },  { id: 'edge-cache', name: 'edge-cache', owner: 'Jun Tanaka', region: 'global', status: 'Live', deploys: 318, uptime: 100, latency: 21, cost: 1430, updated: '2026-10-31' },  { id: 'auth', name: 'auth', owner: 'Mara Lindqvist', region: 'eu-west', status: 'Live', deploys: 176, uptime: 99.99, latency: 66, cost: 540, updated: '2026-10-28' },  { id: 'ingest', name: 'ingest', owner: 'Lena Fischer', region: 'us-east', status: 'Live', deploys: 264, uptime: 99.93, latency: 131, cost: 2120, updated: '2026-10-30' },]type Project = (typeof PROJECTS)[number]const money = (n: number) => `$${n.toLocaleString('en-US')}`const spend = (rows: Project[]) => `Total ${money(rows.reduce((n, p) => n + p.cost, 0))}`const latency = (r: number, h: number) => 40 + ((r + 3) * 37 + h * h * 11) % 260const MONTH_NAMES = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec']const MONTH_COLUMNS = MONTH_NAMES.map((m, i) => ({ id: String(i), header: m }))const swing = (r: number, m: number) => (((r + 2) * 53 + (m + 1) * 29) % 41) - 14const showSwing = (n: number) => `${n > 0 ? '+' : n < 0 ? '−' : ''}${Math.abs(n)}%`

Column widths

Each column sets its own width, and headerWidth sizes the first column.

tsx
import { ScrollGrid } from '@weft-js/warp'export function Widths() {  return (    <ScrollGrid label="Projects" corner="Project" rows={1000} columns={SIZED} rowHeader={r => `${PROJECTS[r % 10]!.name}-${r}`} cell={(r, c) => fieldOf(r, c.id)} headerWidth={160} height="300px" />  )}type Status = 'Live' | 'Building' | 'Failed' | 'Paused'const PROJECTS: { id: string; name: string; owner: string; region: string; status: Status; deploys: number; uptime: number; latency: number; cost: number; updated: string }[] = [  { id: 'storefront', name: 'storefront', owner: 'Priya Raman', region: 'eu-west', status: 'Live', deploys: 412, uptime: 99.98, latency: 84, cost: 1240, updated: '2026-10-31' },  { id: 'checkout', name: 'checkout', owner: 'Tomás Oliveira', region: 'eu-west', status: 'Live', deploys: 288, uptime: 99.95, latency: 112, cost: 980, updated: '2026-10-31' },  { id: 'search-api', name: 'search-api', owner: 'Lena Fischer', region: 'us-east', status: 'Building', deploys: 196, uptime: 99.91, latency: 146, cost: 1610, updated: '2026-10-30' },  { id: 'billing', name: 'billing', owner: 'Mara Lindqvist', region: 'us-east', status: 'Live', deploys: 154, uptime: 99.99, latency: 73, cost: 720, updated: '2026-10-29' },  { id: 'mobile-bff', name: 'mobile-bff', owner: 'Sam Okafor', region: 'ap-south', status: 'Failed', deploys: 233, uptime: 99.42, latency: 208, cost: 860, updated: '2026-10-29' },  { id: 'notifier', name: 'notifier', owner: 'Jun Tanaka', region: 'eu-north', status: 'Live', deploys: 97, uptime: 99.97, latency: 58, cost: 310, updated: '2026-10-27' },  { id: 'reports', name: 'reports', owner: 'Noor Haddad', region: 'us-west', status: 'Paused', deploys: 41, uptime: 99.8, latency: 321, cost: 190, updated: '2026-10-22' },  { id: 'edge-cache', name: 'edge-cache', owner: 'Jun Tanaka', region: 'global', status: 'Live', deploys: 318, uptime: 100, latency: 21, cost: 1430, updated: '2026-10-31' },  { id: 'auth', name: 'auth', owner: 'Mara Lindqvist', region: 'eu-west', status: 'Live', deploys: 176, uptime: 99.99, latency: 66, cost: 540, updated: '2026-10-28' },  { id: 'ingest', name: 'ingest', owner: 'Lena Fischer', region: 'us-east', status: 'Live', deploys: 264, uptime: 99.93, latency: 131, cost: 2120, updated: '2026-10-30' },]const money = (n: number) => `$${n.toLocaleString('en-US')}`const latency = (r: number, h: number) => 40 + ((r + 3) * 37 + h * h * 11) % 260const SIZED = [{ id: 'owner', header: 'Owner', width: 170 }, { id: 'region', header: 'Region', width: 110 }, { id: 'status', header: 'Status', width: 96 }, { id: 'cost', header: 'Cost', width: 96 }]const fieldOf = (r: number, id: string) => {  const p = PROJECTS[r % 10]!  return id === 'cost' ? money(p.cost + r) : p[id as 'owner']}

When to use

Use it for

  • Metrics with many rows and many columns
  • Anything far past what a Table should hold

Reach for something else

Playground

Rows2000
Header width132
import { ScrollGrid } from '@weft-js/warp'<ScrollGrid label="Latency by hour" rows={2000} columns={hours} rowHeader={route} cell={latency} rows={2000} />

Accessibility

↑↓←→
Moves one cell
PageUporPageDown
Moves one screen
HomeorEnd
Goes to the row's ends
⌘Homeor⌘End
Goes to the grid's ends
  • Only the rows in view exist, so aria-rowindex and aria-colindex keep each cell's place in the whole.
  • One tab stop. The active cell is announced as you move.

API Reference

PropTypeDefaultDescription
labelstring—Its accessible name. Fields show it; icon-only controls speak it.
rowsnumber—How many rows there are. Cells are asked for as they come into view.
columnsreadonly GridColumn[]—Each with id, header and an optional width.
cell(row: number, column: GridColumn) => unknown—What a row and column hold.
rowHeader(row: number) => string—The first column's text.
cornerstring—The top-left cell. Default label.
heightstring—Default 480px.
rowHeightnumber—Each row's height, in pixels. Default 34.
headerWidthnumber—The pinned first column's width, in pixels. Default 132.
negative((row: number, column: GridColumn) => boolean)—Marks a cell critical.

Parts

Every element carries a stable name, so you can style it from your own CSS: [data-scope="scroll-grid"] [data-part="cell"]

cellcol-headrowrow-headcanvashead-rowcorner

RelatedData gridVirtual listTable