Component
Data grid
<DomDataGrid>A dense spreadsheet-like grid for database records, with typed column filters, sorting, row selection, and a server-query friendly API. We talk about databases, rendering pipelines, virtual DOMs, and scalable app architecture. But really, they just want Excel.
Demo
Database-style account table
Column filters use typed controls: date columns open a date range, number columns use min/max, select columns use checklist filters, and applied filters are visible in both the header and toolbar.
Customer accounts
15 of 24 accounts
| A | B | C | D | E | F | G | H | I | J | K | L | ||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| # | Filter Account | Filter Status Values | Filter Plan Values | Filter Owner | Filter Seats | Filter ARR | Filter Health Values | Filter Region Values | Filter Renewal | Filter Active users | Filter Verified | Filter Updated | |
| 1 | Anchor Cloud | Active | Enterprise | Sofia Jensen | 264 | £310,000 | Strong | NA | 31 Jul 2026 | 240 | Yes | 11 Jun 2026 | |
| 2 | Orchid Health | Active | Enterprise | Laura Chen | 232 | £268,000 | Strong | NA | 22 Sept 2026 | 211 | Yes | 11 Jun 2026 | |
| 3 | Forge Robotics | Active | Enterprise | Priya Singh | 188 | £224,500 | Strong | APAC | 04 Sept 2026 | 169 | Yes | 10 Jun 2026 | |
| 4 | Mercury Media | Active | Enterprise | Caleb Stone | 176 | £201,300 | Strong | NA | 09 Jul 2026 | 151 | Yes | 09 Jun 2026 | |
| 5 | Northstar Analytics | Active | Enterprise | Amelia Hart | 142 | £184,000 | Strong | EMEA | 19 Aug 2026 | 118 | Yes | 10 Jun 2026 | |
| 6 | Signal Harbor | Active | Scale | Nora Ellis | 96 | £112,900 | Strong | EMEA | 02 Aug 2026 | 88 | Yes | 07 Jun 2026 | |
| 7 | Atlas Finance | Active | Scale | Ravi Patel | 84 | £96,500 | Watch | NA | 28 Jul 2026 | 62 | Yes | 08 Jun 2026 | |
| 8 | Sable Security | Active | Scale | Elena Rossi | 72 | £88,900 | Strong | EMEA | 29 Sept 2026 | 70 | Yes | 02 Jun 2026 | |
| 9 | Violet Manufacturing | Trial | Scale | Kai Foster | 58 | £65,400 | Watch | APAC | 24 Aug 2026 | 43 | No | 01 Jun 2026 | |
| 10 | Summit Grocery | Active | Growth | Maeve Walsh | 36 | £41,800 | Strong | NA | 16 Sept 2026 | 34 | Yes | 06 Jun 2026 | |
| 11 | Bluejay Travel | Trial | Growth | Tara Lewis | 22 | £24,800 | Watch | LATAM | 19 Jul 2026 | 17 | No | 04 Jun 2026 | |
| 12 | Brightwell Studio | Trial | Growth | Mina Okafor | 18 | £21,400 | Strong | EMEA | 14 Sept 2026 | 16 | No | 09 Jun 2026 | |
| 13 | Mosaic Civic | Active | Starter | George White | 16 | £15,800 | Strong | EMEA | 11 Sept 2026 | 15 | Yes | 10 Jun 2026 | |
| 14 | Kite Education | Trial | Starter | Owen Price | 14 | £11,800 | Strong | EMEA | 31 Aug 2026 | 13 | No | 06 Jun 2026 | |
| 15 | Pioneer Retail | Trial | Starter | Miles Brooks | 9 | £7,800 | Watch | NA | 12 Jul 2026 | 6 | No | 03 Jun 2026 |
Resource API shape
The grid can filter locally today, but the same query-change payload can be sent to a resource endpoint for SQL-backed search, where, order, and pagination later.
{
"search": "",
"filters": {
"status": {
"type": "select",
"values": [
"active",
"trial"
]
},
"renewalDate": {
"type": "date",
"start": "2026-07-01",
"end": "2026-09-30"
}
},
"sort": {
"key": "arr",
"direction": "desc"
}
}Large memory
Virtual rows for in-memory data
Use virtualRows when a large local array is already in memory. The grid keeps the full 10,000-row data set available for client-side search, filters, and sort, while the isolated virtual row renderer mounts only the visible rows plus the configured buffer.
Usage events
10000 of 10000 events
| A | B | C | D | E | F | G | H | |
|---|---|---|---|---|---|---|---|---|
| # | Filter Event | Filter Account | Filter Service Values | Filter Status Values | Filter Region Values | Filter Requests | Filter Spend | Filter Updated |
| 1 | 1 | Atlas Works 1 | Billing API | Healthy | UK | 2,500 | £150.00 | 01 Jan 2026 |
| 2 | 2 | Northstar Labs 1 | Delayed | EU | 2,597 | £187.00 | 02 Feb 2026 | |
| 3 | 3 | Pioneer Health 1 | Billing API | Queued | US | 2,694 | £224.00 | 03 Mar 2026 |
| 4 | 4 | Evergreen Finance 1 | Healthy | CA | 2,791 | £261.00 | 04 Apr 2026 | |
| 5 | 5 | Lunar Retail 1 | Billing API | Healthy | AU | 2,888 | £298.00 | 05 May 2026 |
| 6 | 6 | Cobalt Systems 1 | Failed | UK | 2,985 | £335.00 | 06 Jun 2026 | |
| 7 | 7 | Atlas Works 2 | Billing API | Healthy | EU | 3,082 | £372.00 | 07 Jan 2026 |
| 8 | 8 | Northstar Labs 2 | Delayed | US | 3,179 | £409.00 | 08 Feb 2026 | |
| 9 | 9 | Pioneer Health 2 | Billing API | Queued | CA | 3,276 | £446.00 | 09 Mar 2026 |
| 10 | 10 | Evergreen Finance 2 | Healthy | AU | 3,373 | £483.00 | 10 Apr 2026 | |
| 11 | 11 | Lunar Retail 2 | Billing API | Healthy | UK | 3,470 | £520.00 | 11 May 2026 |
| 12 | 12 | Cobalt Systems 2 | Failed | EU | 3,567 | £557.00 | 12 Jun 2026 | |
| 13 | 13 | Atlas Works 3 | Billing API | Healthy | US | 3,664 | £594.00 | 13 Jan 2026 |
| 14 | 14 | Northstar Labs 3 | Delayed | CA | 3,761 | £631.00 | 14 Feb 2026 | |
| 15 | 15 | Pioneer Health 3 | Billing API | Queued | AU | 3,858 | £668.00 | 15 Mar 2026 |
| 16 | 16 | Evergreen Finance 3 | Healthy | UK | 3,955 | £705.00 | 16 Apr 2026 | |
| 17 | 17 | Lunar Retail 3 | Billing API | Healthy | EU | 4,052 | £742.00 | 17 May 2026 |
| 18 | 18 | Cobalt Systems 3 | Failed | US | 4,149 | £779.00 | 18 Jun 2026 | |
| 19 | 19 | Atlas Works 4 | Billing API | Healthy | CA | 4,246 | £816.00 | 19 Jan 2026 |
| 20 | 20 | Northstar Labs 4 | Delayed | AU | 4,343 | £853.00 | 20 Feb 2026 | |
| 21 | 21 | Pioneer Health 4 | Billing API | Queued | UK | 4,440 | £890.00 | 21 Mar 2026 |
| 22 | 22 | Evergreen Finance 4 | Healthy | EU | 4,537 | £927.00 | 22 Apr 2026 | |
| 23 | 23 | Lunar Retail 4 | Billing API | Healthy | US | 4,634 | £964.00 | 23 May 2026 |
| 24 | 24 | Cobalt Systems 4 | Failed | CA | 4,731 | £1,001 | 24 Jun 2026 | |
| 25 | 25 | Atlas Works 5 | Billing API | Healthy | AU | 4,828 | £1,038 | 25 Jan 2026 |
| 26 | 26 | Northstar Labs 5 | Delayed | UK | 4,925 | £1,075 | 26 Feb 2026 | |
| 27 | 27 | Pioneer Health 5 | Billing API | Queued | EU | 5,022 | £1,112 | 27 Mar 2026 |
| 28 | 28 | Evergreen Finance 5 | Healthy | US | 5,119 | £1,149 | 28 Apr 2026 | |
| 29 | 29 | Lunar Retail 5 | Billing API | Healthy | CA | 5,216 | £1,186 | 01 May 2026 |
| 30 | 30 | Cobalt Systems 5 | Failed | AU | 5,313 | £1,223 | 02 Jun 2026 | |
| 31 | 31 | Atlas Works 6 | Billing API | Healthy | UK | 5,410 | £1,260 | 03 Jan 2026 |
| 32 | 32 | Northstar Labs 6 | Delayed | EU | 5,507 | £1,297 | 04 Feb 2026 | |
| 33 | 33 | Pioneer Health 6 | Billing API | Queued | US | 5,604 | £1,334 | 05 Mar 2026 |
| 34 | 34 | Evergreen Finance 6 | Healthy | CA | 5,701 | £1,371 | 06 Apr 2026 | |
| 35 | 35 | Lunar Retail 6 | Billing API | Healthy | AU | 5,798 | £1,408 | 07 May 2026 |
| 36 | 36 | Cobalt Systems 6 | Failed | UK | 5,895 | £1,445 | 08 Jun 2026 | |
| 37 | 37 | Atlas Works 7 | Billing API | Healthy | EU | 5,992 | £1,482 | 09 Jan 2026 |
| 38 | 38 | Northstar Labs 7 | Delayed | US | 6,089 | £1,519 | 10 Feb 2026 | |
| 39 | 39 | Pioneer Health 7 | Billing API | Queued | CA | 6,186 | £1,556 | 11 Mar 2026 |
| 40 | 40 | Evergreen Finance 7 | Healthy | AU | 6,283 | £1,593 | 12 Apr 2026 | |
| 41 | 41 | Lunar Retail 7 | Billing API | Healthy | UK | 6,380 | £1,630 | 13 May 2026 |
| 42 | 42 | Cobalt Systems 7 | Failed | EU | 6,477 | £1,667 | 14 Jun 2026 | |
| 43 | 43 | Atlas Works 8 | Billing API | Healthy | US | 6,574 | £1,704 | 15 Jan 2026 |
| 44 | 44 | Northstar Labs 8 | Delayed | CA | 6,671 | £1,741 | 16 Feb 2026 | |
| 45 | 45 | Pioneer Health 8 | Billing API | Queued | AU | 6,768 | £1,778 | 17 Mar 2026 |
| 46 | 46 | Evergreen Finance 8 | Healthy | UK | 6,865 | £1,815 | 18 Apr 2026 | |
| 47 | 47 | Lunar Retail 8 | Billing API | Healthy | EU | 6,962 | £1,852 | 19 May 2026 |
| 48 | 48 | Cobalt Systems 8 | Failed | US | 7,059 | £1,889 | 20 Jun 2026 | |
| 49 | 49 | Atlas Works 9 | Billing API | Healthy | CA | 7,156 | £1,926 | 21 Jan 2026 |
| 50 | 50 | Northstar Labs 9 | Delayed | AU | 7,253 | £1,963 | 22 Feb 2026 | |
| 51 | 51 | Pioneer Health 9 | Billing API | Queued | UK | 7,350 | £2,000 | 23 Mar 2026 |
Advanced sorting
Keyword table sort presets
Enable multiSort when header clicks should toggle ordered sort rules per column. Click Volume to add it, click Difficulty to add a second rule, and click a sorted header through asc, desc, and off.
Keyword opportunity sort
The selected preset sends ordered sort rules, so APIs can translate them to stable ORDER BY clauses.
Keyword research
10 of 10 keywords
| A | B | C | D | E | F | |
|---|---|---|---|---|---|---|
| # | Filter Keyword | Filter Intent Values | Filter Volume | Filter Difficulty | Filter Opportunity | Filter CPC |
| 1 | vue data grid | Commercial | 5,400 | 28 | 3,942 | $5.40 |
| 2 | javascript spreadsheet component | Comparison | 5,400 | 43 | 3,132 | $7.10 |
| 3 | editable table vue | Commercial | 2,900 | 21 | 2,320 | $4.80 |
| 4 | tailwind data table | Informational | 8,100 | 61 | 3,240 | $3.90 |
| 5 | server side data grid | Commercial | 3,600 | 35 | 2,376 | $8.20 |
| 6 | open source data grid vue | Comparison | 4,400 | 24 | 3,388 | $4.60 |
| 7 | keyword clustering tool | Transactional | 6,600 | 48 | 3,498 | $6.70 |
| 8 | saas dashboard table | Informational | 1,900 | 17 | 1,596 | $3.20 |
| 9 | best vue table library | Comparison | 5,400 | 31 | 3,780 | $5.90 |
| 10 | crm data grid | Commercial | 2,400 | 19 | 1,968 | $9.40 |
{
"search": "",
"sort": {
"key": "",
"direction": "",
"rules": []
}
}Remote data
Server pagination and forever scrolling
The same resource endpoint handles page-number navigation and cursor-based forever scrolling. The grid sends search, filters, and sort; pagination adds either page/pageSize or cursor/limit.
Remote customer resource
The grid asks for rows; the mock API applies search, filters, sort, and pagination.
Customers
0 of 0 customers from resource API
| A | B | C | D | E | F | G | ||
|---|---|---|---|---|---|---|---|---|
| # | Filter Customer | Filter Status Values | Filter Owner | Filter Region Values | Filter Seats | Filter ARR | Filter Updated | |
| No rows match the current filters. | ||||||||
Request
{
"resource": "customers",
"query": {
"search": "",
"filters": {},
"sort": {
"key": "updatedAt",
"direction": "desc"
}
},
"pagination": {
"strategy": "page",
"page": 1,
"pageSize": 8
}
}Response
{
"data": [],
"pageInfo": {
"strategy": "page",
"page": 1,
"pageSize": 8,
"pageCount": 1,
"total": 0,
"hasNextPage": false,
"hasPreviousPage": false,
"nextCursor": ""
},
"meta": {
"total": 0,
"returned": 0
}
}Live data
SQL query log with duration ordering
A live developer query-log surface that treats the grid as a server-backed resource: new query rows stream into the buffer, search and typed filters update the resource query, and duration_ms desc exposes the slowest statements first.
Buffered
140
P95 duration
966 ms
Slow queries
32
Errors
4
SQL log
80 of 140 queries from resource API
| # | Filter Database Values | Filter Service Values | Filter Verb Values | Filter Table Values | Filter Duration ms | Filter Lock wait | Filter Rows examined | Filter Status Values | Filter Plan Values | Filter Endpoint | Filter SQL statement | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 1 | 11:48:54 | Billing | Billing sync | UPDATE | audit_events | 1749 ms | 66 ms | 731 | Slow | Index scan | stripe:update | update audit_events set updated_at = now(), metadata = jsonb_set(metadata, ?, ?) where organisation_id = ? and id = ? |
| 2 | 11:49:09 | Primary writer | Worker | UPDATE | invoices | 1398 ms | 195 ms | 632 | Slow | Index scan | job:invoices:sync | update invoices set updated_at = now(), metadata = jsonb_set(metadata, ?, ?) where organisation_id = ? and id = ? |
| 3 | 11:44:26 | Analytics | Billing sync | SELECT | website_pages | 1338 ms | 0 ms | 130,334 | Slow | Seq scan | stripe:select | select * from website_pages where organisation_id = ? and lower(search_text) like ? order by updated_at desc limit 100 |
| 4 | 11:44:54 | Billing | Reporting | UPDATE | accounts | 1336 ms | 496 ms | 361 | Slow | Nested loop | /reports/accounts | update accounts set updated_at = now(), metadata = jsonb_set(metadata, ?, ?) where organisation_id = ? and id = ? |
| 5 | 11:43:48 | Primary writer | Billing sync | UPDATE | accounts | 1117 ms | 95 ms | 1,157 | Slow | Index scan | stripe:update | update accounts set updated_at = now(), metadata = jsonb_set(metadata, ?, ?) where organisation_id = ? and id = ? |
| 6 | 11:48:12 | Primary writer | App API | SELECT | website_pages | 988 ms | 471 ms | 99,856 | Slow | Seq scan | /api/website_pages | select * from website_pages where organisation_id = ? and lower(search_text) like ? order by updated_at desc limit 100 |
| 7 | 11:44:37 | Read replica | App API | UPDATE | users | 966 ms | 0 ms | 1,040 | Slow | Index scan | /api/users | update users set updated_at = now(), metadata = jsonb_set(metadata, ?, ?) where organisation_id = ? and id = ? |
| 8 | 11:43:38 | Read replica | Worker | SELECT | accounts | 966 ms | 143 ms | 304,156 | Slow | Seq scan | job:accounts:sync | select * from accounts where organisation_id = ? and lower(search_text) like ? order by updated_at desc limit 100 |
| 9 | 11:49:57 | Read replica | Reporting | SELECT | sessions | 911 ms | 293 ms | 248,335 | Slow | Seq scan | /reports/sessions | select * from sessions where organisation_id = ? and lower(search_text) like ? order by updated_at desc limit 100 |
| 10 | 11:46:41 | Analytics | Webhook | SELECT | organisations | 883 ms | 0 ms | 163,019 | Slow | Seq scan | /webhooks/organisations | select * from organisations where organisation_id = ? and lower(search_text) like ? order by updated_at desc limit 100 |
| 11 | 11:43:40 | Primary writer | Worker | SELECT | sessions | 827 ms | 0 ms | 656,218 | Slow | Seq scan | job:sessions:sync | select * from sessions where organisation_id = ? and lower(search_text) like ? order by updated_at desc limit 100 |
| 12 | 11:50:08 | Analytics | Billing sync | SELECT | jobs | 778 ms | 0 ms | 384,063 | Slow | Seq scan | stripe:select | select * from jobs where organisation_id = ? and lower(search_text) like ? order by updated_at desc limit 100 |
| 13 | 11:48:07 | Billing | Reporting | INSERT | website_pages | 776 ms | 421 ms | 705 | Slow | Index scan | /reports/website_pages | insert into website_pages (organisation_id, payload, created_at) values (?, ?, now()) returning id |
| 14 | 11:48:19 | Primary writer | Webhook | UPDATE | users | 700 ms | 0 ms | 218 | Slow | Nested loop | /webhooks/users | update users set updated_at = now(), metadata = jsonb_set(metadata, ?, ?) where organisation_id = ? and id = ? |
| 15 | 11:43:29 | Read replica | Reporting | SELECT | billing_events | 570 ms | 0 ms | 365,953 | Slow | Seq scan | /reports/billing_events | select * from billing_events where organisation_id = ? and lower(search_text) like ? order by updated_at desc limit 100 |
| 16 | 11:42:23 | Read replica | Billing sync | UPDATE | organisations | 503 ms | 9 ms | 214 | Slow | Nested loop | stripe:update | update organisations set updated_at = now(), metadata = jsonb_set(metadata, ?, ?) where organisation_id = ? and id = ? |
| 17 | 11:47:37 | Primary writer | Billing sync | UPDATE | website_pages | 385 ms | 19 ms | 1,170 | Slow | Nested loop | stripe:update | update website_pages set updated_at = now(), metadata = jsonb_set(metadata, ?, ?) where organisation_id = ? and id = ? |
| 18 | 11:45:50 | Read replica | Webhook | INSERT | organisations | 317 ms | 142 ms | 11 | Slow | Index scan | /webhooks/organisations | insert into organisations (organisation_id, payload, created_at) values (?, ?, now()) returning id |
| 19 | 11:48:13 | Read replica | Billing sync | INSERT | users | 316 ms | 0 ms | 274 | Slow | Index scan | stripe:insert | insert into users (organisation_id, payload, created_at) values (?, ?, now()) returning id |
| 20 | 11:46:54 | Read replica | Reporting | DELETE | jobs | 314 ms | 0 ms | 865 | Slow | Index scan | /reports/jobs | delete from jobs where organisation_id = ? and archived_at < now() - interval '90 days' |
| 21 | 11:45:46 | Primary writer | Webhook | UPDATE | feature_flags | 313 ms | 119 ms | 833 | Slow | Nested loop | /webhooks/feature_flags | update feature_flags set updated_at = now(), metadata = jsonb_set(metadata, ?, ?) where organisation_id = ? and id = ? |
| 22 | 11:42:35 | Analytics | Webhook | UPDATE | content_items | 304 ms | 0 ms | 107 | Slow | Index scan | /webhooks/content_items | update content_items set updated_at = now(), metadata = jsonb_set(metadata, ?, ?) where organisation_id = ? and id = ? |
| 23 | 11:50:04 | Read replica | Webhook | INSERT | users | 301 ms | 67 ms | 653 | Slow | Index scan | /webhooks/users | insert into users (organisation_id, payload, created_at) values (?, ?, now()) returning id |
| 24 | 11:45:03 | Read replica | Billing sync | INSERT | jobs | 291 ms | 0 ms | 237 | Slow | Index scan | stripe:insert | insert into jobs (organisation_id, payload, created_at) values (?, ?, now()) returning id |
| 25 | 11:41:06 | Primary writer | Worker | DELETE | website_pages | 289 ms | 0 ms | 62 | Slow | Index scan | job:website_pages:sync | delete from website_pages where organisation_id = ? and archived_at < now() - interval '90 days' |
| 26 | 11:49:55 | Billing | Billing sync | UPDATE | sessions | 283 ms | 0 ms | 1,060 | Slow | Nested loop | stripe:update | update sessions set updated_at = now(), metadata = jsonb_set(metadata, ?, ?) where organisation_id = ? and id = ? |
| 27 | 11:45:30 | Billing | Billing sync | UPDATE | invoices | 280 ms | 139 ms | 92 | Slow | Index scan | stripe:update | update invoices set updated_at = now(), metadata = jsonb_set(metadata, ?, ?) where organisation_id = ? and id = ? |
| 28 | 11:49:08 | Primary writer | Reporting | INSERT | jobs | 277 ms | 68 ms | 689 | Slow | Index scan | /reports/jobs | insert into jobs (organisation_id, payload, created_at) values (?, ?, now()) returning id |
| 29 | 11:43:40 | Billing | Reporting | UPDATE | organisations | 274 ms | 0 ms | 320 | Slow | Index scan | /reports/organisations | update organisations set updated_at = now(), metadata = jsonb_set(metadata, ?, ?) where organisation_id = ? and id = ? |
| 30 | 11:47:40 | Primary writer | Worker | UPDATE | organisations | 267 ms | 127 ms | 802 | Slow | Index scan | job:organisations:sync | update organisations set updated_at = now(), metadata = jsonb_set(metadata, ?, ?) where organisation_id = ? and id = ? |
| 31 | 11:47:30 | Read replica | Reporting | INSERT | organisations | 263 ms | 0 ms | 471 | Slow | Index scan | /reports/organisations | insert into organisations (organisation_id, payload, created_at) values (?, ?, now()) returning id |
| 32 | 11:47:49 | Analytics | Reporting | UPDATE | billing_events | 250 ms | 0 ms | 1,110 | Slow | Index scan | /reports/billing_events | update billing_events set updated_at = now(), metadata = jsonb_set(metadata, ?, ?) where organisation_id = ? and id = ? |
| 33 | 11:49:13 | Analytics | Webhook | INSERT | billing_events | 219 ms | 0 ms | 145 | OK | Index scan | /webhooks/billing_events | insert into billing_events (organisation_id, payload, created_at) values (?, ?, now()) returning id |
| 34 | 11:48:37 | Billing | Reporting | INSERT | billing_events | 207 ms | 0 ms | 1,139 | OK | Nested loop | /reports/billing_events | insert into billing_events (organisation_id, payload, created_at) values (?, ?, now()) returning id |
| 35 | 11:44:00 | Primary writer | Billing sync | DELETE | accounts | 199 ms | 0 ms | 687 | OK | Index scan | stripe:delete | delete from accounts where organisation_id = ? and archived_at < now() - interval '90 days' |
| 36 | 11:48:57 | Billing | Webhook | DELETE | audit_events | 196 ms | 41 ms | 179 | OK | Index scan | /webhooks/audit_events | delete from audit_events where organisation_id = ? and archived_at < now() - interval '90 days' |
| 37 | 11:46:30 | Analytics | App API | INSERT | sessions | 195 ms | 85 ms | 953 | OK | Index scan | /api/sessions | insert into sessions (organisation_id, payload, created_at) values (?, ?, now()) returning id |
| 38 | 11:46:13 | Read replica | Billing sync | UPDATE | jobs | 191 ms | 0 ms | 423 | OK | Nested loop | stripe:update | update jobs set updated_at = now(), metadata = jsonb_set(metadata, ?, ?) where organisation_id = ? and id = ? |
| 39 | 11:50:29 | Analytics | App API | SELECT | users | 188 ms | 0 ms | 4,165 | OK | Index scan | /api/users | select id, organisation_id, status, updated_at from users where organisation_id = ? and id = ? limit 1 |
| 40 | 11:46:10 | Billing | Worker | UPDATE | website_pages | 186 ms | 91 ms | 1,015 | OK | Index scan | job:website_pages:sync | update website_pages set updated_at = now(), metadata = jsonb_set(metadata, ?, ?) where organisation_id = ? and id = ? |
| 41 | 11:48:48 | Primary writer | Billing sync | SELECT | invoices | 185 ms | 0 ms | 4,928 | Error | Index scan | stripe:select | select id, organisation_id, status, updated_at from invoices where organisation_id = ? and id = ? limit 1 |
| 42 | 11:47:33 | Primary writer | App API | UPDATE | sessions | 181 ms | 0 ms | 479 | OK | Index scan | /api/sessions | update sessions set updated_at = now(), metadata = jsonb_set(metadata, ?, ?) where organisation_id = ? and id = ? |
| 43 | 11:40:52 | Billing | Billing sync | SELECT | website_pages | 181 ms | 0 ms | 6,404 | OK | Index scan | stripe:select | select id, organisation_id, status, updated_at from website_pages where organisation_id = ? and id = ? limit 1 |
| 44 | 11:45:48 | Analytics | Billing sync | UPDATE | accounts | 176 ms | 0 ms | 736 | OK | Index scan | stripe:update | update accounts set updated_at = now(), metadata = jsonb_set(metadata, ?, ?) where organisation_id = ? and id = ? |
| 45 | 11:49:26 | Billing | Billing sync | SELECT | sessions | 174 ms | 0 ms | 644 | OK | Index scan | stripe:select | select id, organisation_id, status, updated_at from sessions where organisation_id = ? and id = ? limit 1 |
| 46 | 11:50:51 | Primary writer | Worker | SELECT | accounts | 172 ms | 0 ms | 5,160 | OK | Index scan | job:accounts:sync | select id, organisation_id, status, updated_at from accounts where organisation_id = ? and id = ? limit 1 |
| 47 | 11:49:19 | Billing | Webhook | SELECT | accounts | 168 ms | 0 ms | 5,998 | Error | Index scan | /webhooks/accounts | select id, organisation_id, status, updated_at from accounts where organisation_id = ? and id = ? limit 1 |
| 48 | 11:49:17 | Primary writer | Webhook | SELECT | jobs | 168 ms | 0 ms | 797 | OK | Index scan | /webhooks/jobs | select id, organisation_id, status, updated_at from jobs where organisation_id = ? and id = ? limit 1 |
| 49 | 11:48:09 | Billing | Billing sync | SELECT | users | 168 ms | 0 ms | 3,118 | OK | Index scan | stripe:select | select id, organisation_id, status, updated_at from users where organisation_id = ? and id = ? limit 1 |
| 50 | 11:46:39 | Billing | Reporting | SELECT | invoices | 168 ms | 0 ms | 6,566 | OK | Index scan | /reports/invoices | select id, organisation_id, status, updated_at from invoices where organisation_id = ? and id = ? limit 1 |
| 51 | 11:49:49 | Analytics | Webhook | UPDATE | jobs | 167 ms | 24 ms | 204 | OK | Index scan | /webhooks/jobs | update jobs set updated_at = now(), metadata = jsonb_set(metadata, ?, ?) where organisation_id = ? and id = ? |
| 52 | 11:48:53 | Analytics | Billing sync | SELECT | feature_flags | 167 ms | 0 ms | 2,337 | OK | Index scan | stripe:select | select id, organisation_id, status, updated_at from feature_flags where organisation_id = ? and id = ? limit 1 |
| 53 | 11:48:35 | Primary writer | Worker | SELECT | billing_events | 166 ms | 0 ms | 6,939 | OK | Index scan | job:billing_events:sync | select id, organisation_id, status, updated_at from billing_events where organisation_id = ? and id = ? limit 1 |
| 54 | 11:49:45 | Primary writer | App API | INSERT | users | 162 ms | 0 ms | 402 | OK | Index scan | /api/users | insert into users (organisation_id, payload, created_at) values (?, ?, now()) returning id |
| 55 | 11:48:38 | Read replica | Billing sync | INSERT | sessions | 160 ms | 0 ms | 956 | OK | Index scan | stripe:insert | insert into sessions (organisation_id, payload, created_at) values (?, ?, now()) returning id |
| 56 | 11:41:56 | Primary writer | Webhook | SELECT | accounts | 160 ms | 0 ms | 1,224 | OK | Index scan | /webhooks/accounts | select id, organisation_id, status, updated_at from accounts where organisation_id = ? and id = ? limit 1 |
| 57 | 11:46:02 | Analytics | Webhook | INSERT | accounts | 154 ms | 17 ms | 709 | OK | Index scan | /webhooks/accounts | insert into accounts (organisation_id, payload, created_at) values (?, ?, now()) returning id |
| 58 | 11:49:20 | Billing | Billing sync | SELECT | jobs | 153 ms | 0 ms | 6,144 | OK | Index scan | stripe:select | select id, organisation_id, status, updated_at from jobs where organisation_id = ? and id = ? limit 1 |
| 59 | 11:44:56 | Analytics | Reporting | SELECT | audit_events | 149 ms | 0 ms | 912 | OK | Index scan | /reports/audit_events | select id, organisation_id, status, updated_at from audit_events where organisation_id = ? and id = ? limit 1 |
| 60 | 11:45:18 | Read replica | Worker | DELETE | audit_events | 145 ms | 0 ms | 372 | OK | Index scan | job:audit_events:sync | delete from audit_events where organisation_id = ? and archived_at < now() - interval '90 days' |
| 61 | 11:50:37 | Read replica | Webhook | INSERT | jobs | 144 ms | 0 ms | 662 | OK | Index scan | /webhooks/jobs | insert into jobs (organisation_id, payload, created_at) values (?, ?, now()) returning id |
| 62 | 11:48:34 | Analytics | Reporting | SELECT | accounts | 144 ms | 0 ms | 5,928 | OK | Index scan | /reports/accounts | select id, organisation_id, status, updated_at from accounts where organisation_id = ? and id = ? limit 1 |
| 63 | 11:42:51 | Read replica | Worker | SELECT | audit_events | 144 ms | 0 ms | 3,014 | OK | Index scan | job:audit_events:sync | select id, organisation_id, status, updated_at from audit_events where organisation_id = ? and id = ? limit 1 |
| 64 | 11:47:34 | Analytics | Billing sync | UPDATE | organisations | 142 ms | 41 ms | 217 | OK | Index scan | stripe:update | update organisations set updated_at = now(), metadata = jsonb_set(metadata, ?, ?) where organisation_id = ? and id = ? |
| 65 | 11:45:28 | Primary writer | Billing sync | SELECT | audit_events | 142 ms | 0 ms | 7,828 | OK | Index scan | stripe:select | select id, organisation_id, status, updated_at from audit_events where organisation_id = ? and id = ? limit 1 |
| 66 | 11:45:28 | Billing | App API | SELECT | billing_events | 142 ms | 0 ms | 5,691 | OK | Index scan | /api/billing_events | select id, organisation_id, status, updated_at from billing_events where organisation_id = ? and id = ? limit 1 |
| 67 | 11:43:28 | Analytics | Reporting | SELECT | feature_flags | 137 ms | 0 ms | 8,976 | OK | Index scan | /reports/feature_flags | select id, organisation_id, status, updated_at from feature_flags where organisation_id = ? and id = ? limit 1 |
| 68 | 11:48:30 | Billing | Webhook | SELECT | users | 133 ms | 0 ms | 4,874 | OK | Index scan | /webhooks/users | select id, organisation_id, status, updated_at from users where organisation_id = ? and id = ? limit 1 |
| 69 | 11:42:33 | Primary writer | Reporting | SELECT | audit_events | 133 ms | 0 ms | 9,328 | OK | Index scan | /reports/audit_events | select id, organisation_id, status, updated_at from audit_events where organisation_id = ? and id = ? limit 1 |
| 70 | 11:50:15 | Analytics | App API | INSERT | billing_events | 131 ms | 0 ms | 350 | OK | Index scan | /api/billing_events | insert into billing_events (organisation_id, payload, created_at) values (?, ?, now()) returning id |
| 71 | 11:49:46 | Analytics | Worker | INSERT | feature_flags | 130 ms | 0 ms | 698 | OK | Nested loop | job:feature_flags:sync | insert into feature_flags (organisation_id, payload, created_at) values (?, ?, now()) returning id |
| 72 | 11:41:02 | Primary writer | Reporting | SELECT | content_items | 129 ms | 0 ms | 638 | OK | Index scan | /reports/content_items | select id, organisation_id, status, updated_at from content_items where organisation_id = ? and id = ? limit 1 |
| 73 | 11:49:17 | Billing | Worker | SELECT | sessions | 127 ms | 0 ms | 6,213 | OK | Index scan | job:sessions:sync | select id, organisation_id, status, updated_at from sessions where organisation_id = ? and id = ? limit 1 |
| 74 | 11:43:12 | Primary writer | Webhook | SELECT | content_items | 127 ms | 0 ms | 4,496 | OK | Index scan | /webhooks/content_items | select id, organisation_id, status, updated_at from content_items where organisation_id = ? and id = ? limit 1 |
| 75 | 11:50:01 | Billing | Worker | UPDATE | invoices | 116 ms | 0 ms | 605 | OK | Index scan | job:invoices:sync | update invoices set updated_at = now(), metadata = jsonb_set(metadata, ?, ?) where organisation_id = ? and id = ? |
| 76 | 11:49:51 | Analytics | Billing sync | SELECT | jobs | 116 ms | 0 ms | 1,118 | OK | Index scan | stripe:select | select id, organisation_id, status, updated_at from jobs where organisation_id = ? and id = ? limit 1 |
| 77 | 11:48:15 | Primary writer | Reporting | SELECT | accounts | 116 ms | 0 ms | 1,782 | OK | Index scan | /reports/accounts | select id, organisation_id, status, updated_at from accounts where organisation_id = ? and id = ? limit 1 |
| 78 | 11:50:27 | Read replica | Webhook | UPDATE | billing_events | 114 ms | 0 ms | 406 | OK | Index scan | /webhooks/billing_events | update billing_events set updated_at = now(), metadata = jsonb_set(metadata, ?, ?) where organisation_id = ? and id = ? |
| 79 | 11:50:05 | Primary writer | App API | SELECT | jobs | 114 ms | 0 ms | 6,898 | OK | Index scan | /api/jobs | select id, organisation_id, status, updated_at from jobs where organisation_id = ? and id = ? limit 1 |
| 80 | 11:49:32 | Analytics | Reporting | SELECT | content_items | 114 ms | 0 ms | 5,062 | OK | Index scan | /reports/content_items | select id, organisation_id, status, updated_at from content_items where organisation_id = ? and id = ? limit 1 |
SQL generated from grid state
select id, started_at, database_name, service_name, operation, table_name, duration_ms, lock_wait_ms, rows_examined, status, plan, endpoint, statement from observability.query_log order by duration_ms DESC limit 80;
Request
{
"resource": "observability.query_log",
"query": {
"search": "",
"filters": {},
"sort": {
"key": "durationMs",
"direction": "desc",
"rules": [
{
"key": "durationMs",
"direction": "desc"
}
]
}
},
"pagination": {
"strategy": "live-window",
"limit": 80,
"buffered": 140
}
}Response window
{
"returned": 80,
"total": 140,
"buffered": 140,
"lastUpdatedAt": "11:50:57"
}Custom cells
Column components and row actions
Set column.component to a Vue component. The component receives row, column, value, formatted, rowIndex, and rowKey. Display-only columns such as actions still use a stable key, but do not need a matching row property.
Team members
6 of 6 users
Filter User | Filter Team | Filter Status Values | Filter Last active | |
|---|---|---|---|---|
| Product | Pending | 17 Jun 2026 | ||
| Engineering | Active | 16 Jun 2026 | ||
| Design | Active | 12 Jun 2026 | ||
| Support | Paused | 08 Jun 2026 | ||
| Operations | Pending | 06 Jun 2026 | ||
| Sales | Active | 04 Jun 2026 |
EditNameCell.vue
RowActionsCell.vue
Architecture
Resource query API
The component filters client-side by default so it works immediately with local rows. Set remote when the grid should render API results exactly as supplied and use query-change to request a SQL-backed resource. Keep pagination beside the grid query so the same endpoint can support numbered pages, previous/next controls, and cursor-fed scrolling.
Use virtualRows when rows are already held in memory but the DOM should stay small. It swaps the table body to a dedicated virtual row renderer, using virtualRowBuffer to keep extra rows mounted above and below the viewport.
For custom rendering, add component to a column definition. The column key is the stable column id; set field when the component should read a different row property, or omit it for display-only columns such as row actions.
Column definitions are the contract. A date column gets a date-range filter, numeric columns get min/max, boolean columns get a yes/no segmented control, and select columns can render known options from the resource schema. Set filterable: false on display-only or operational columns so they stay out of the filter UI and resource query.
For more complex ordering, pass sort.rules as an ordered array, or enable multiSort so header clicks build that array interactively. A keyword table can send searchVolume desc followed by difficulty asc, and a resource API can translate that directly to a multi-column ORDER BY.
Reference
Props
Control props
| Name | Type | TS | Default | Description |
|---|---|---|---|---|
rows | array | Array<unknown> | [] | Rows to render. Each row should be a plain object from an API, store, or database result. |
columns | array | Array< | [] | Column definitions. Each type chooses its own sorting, formatting, and filter controls. |
rowKey | string | function | string | 'id' | Field name or function used to identify each row. |
filters | object | Record<string, unknown> | {} | Controlled filter map keyed by column key. |
sort | object | array | Array<unknown> | {"key":"","direction":""} | Controlled sort state as { key, direction }, or { rules } for multi-column and computed sorting. |
multiSort | boolean | boolean | false | When true, header clicks toggle each sortable column inside an ordered sort.rules array. |
search | string | string | '' | Global search text. |
selectedKeys | array | Array<unknown> | [] | Selected row keys. |
remote | boolean | boolean | false | When true, rows render as supplied and query-change can drive a server/API request. |
loading | boolean | boolean | false | Show a loading row while server data is being fetched. |
virtualRows | boolean | boolean | false | Render large in-memory row sets through the isolated virtual row renderer. |
virtualRowBuffer | number | number | 50 | Rows kept mounted above and below the visible viewport when virtualRows is true. |
virtualRowHeight | number | number | — | Fixed row height in pixels for virtual row calculations. Defaults to the grid density. |
selectable | boolean | boolean | true | Show row selection checkboxes. |
showRowNumbers | boolean | boolean | true | Show spreadsheet-style row numbers. |
showColumnLetters | boolean | boolean | true | Show spreadsheet-style column letters above headers. |
stickyHeader | boolean | boolean | true | Keep header rows visible while the grid scrolls. |
resizableColumns | boolean | boolean | true | Allow users to drag header dividers to resize columns. |
minColumnWidth | number | number | 40 | Minimum column width in pixels while resizing. The default leaves room for the filter icon. |
toolbar | boolean | boolean | true | Show the title, search box, filter chips, and row count toolbar. |
title | string | string | 'Data grid' | Toolbar title. |
resourceLabel | string | string | 'rows' | Human label for the resource being rendered. |
totalRows | number | number | — | Server-side total row count when remote is true. |
density | 'compact' | 'comfortable' | string | 'compact' | Row height density. |
height | string | number | string | '34rem' | Scrollable grid height as a CSS size or pixel number. |
emptyText | string | string | 'No rows match the current filters.' | Message shown when no rows are visible. |
Auto-generated from Data grid.props and inline _edit hints.
Events
| Name | Payload | Description |
|---|---|---|
| @update:filters | Record<string, DataGridFilter> | Fired when a column filter is applied or cleared. |
| @filter-change | ( | — |
| @update:sort | DataGridSort | Fired when column sorting changes. Complex sort values may include an ordered rules array. |
| @sort-change | ( | — |
| @update:search | string | Fired when the toolbar search changes. |
| @query-change | DataGridQuery | Fired with the normalized search, filters, and sort state. Use this to call a resource API. |
| @viewport-scroll | { scrollTop, scrollHeight, clientHeight, remaining, atEnd } | Fired when the scrollable row viewport scrolls. |
| @viewport-end | { scrollTop, scrollHeight, clientHeight, remaining, atEnd } | Fired when the scrollable row viewport reaches the bottom threshold. Useful for cursor pagination and infinite loading. |
| @cell-update | ({ row, column, rowIndex, rowKey, value, previousValue }) | Re-emitted when a column component emits update:value. |
| @cell-action | ({ row, column, rowIndex, rowKey, value, formatted, action, payload }) | Re-emitted when a column component emits action. |
| @column-resize | ({ column, key, width, previousWidth, startWidth }) | Fired while a header divider is dragged and a column width changes. |
| @column-resize-end | ({ column, key, width, previousWidth, startWidth }) | Fired when the user finishes resizing a column. |
| @render-window-change | ({ startIndex, endIndex, renderedRows, totalRows, firstVisibleIndex, lastVisibleIndex }) | Fired by the virtual row renderer when the mounted row window changes. |
| @update:selectedKeys | Array<string | number> | Fired when row selection changes. |
| @selection-change | ( | — |
| @row-click | ({ row, index }) | Fired when a data row is clicked. |
Names auto-detected from defineEmits and source emit() calls; payload and description from __doc.events when present.
Slots
| Name | Scope | Description |
|---|---|---|
| #cell | { row, column, value, formatted, rowIndex, rowKey } | Custom cell renderer. |
| #toolbar-actions | { query, clearFilters } | Extra controls rendered next to search and Clear filters. |
| #empty | — | Custom empty state. |