Grids

The data grid is the table your application is built around: orders, customers, stock, tickets, trades. It binds to an array, a URL of JSON or CSV, a table already in your page, or your own server a block at a time — and a million rows scroll as smoothly as a hundred, because only the rows and columns in view are ever drawn.

Any data source: JavaScript arrays, REST and JSON APIs, CSV, TSV and JSON-lines files, a file the reader opens, an HTML table in the page, your own server a block at a time, and live WebSocket or server-sent events feeds — try them.

Fifty column types each know how to show, sort, filter and edit their values: money in the reader's currency and locale, dates that stay on their day in every time zone, status badges, progress bars, rings, stars, heat maps, data bars, trends, sparklines, people, pictures, tags, files, due dates, buttons and row actions.

Sort by several columns, search every column at once, filter with >100, 2026-09 or last 7 days, choose from a column's own values, and drag a heading to the group bar to group — with totals in every group and the footer. It is editable unless you say otherwise: every edit is validated, can be refused by your page, undone, and reported for saving.

And the rest of what business screens ask for: trees, master and detail rows, a pivot, rows dragged into order, a fill handle, a context menu, conditional formatting written as data, pinned rows, cards on a phone, pages, printing, Excel files in the grid's own colours, and a live feed over WebSocket or server-sent events.

Any data source

The grid takes your data in whatever it already is, and reads the columns from it when you give none. Press a button to load the same grid from each: every one of them here is made up in this page, so nothing leaves it.

A JavaScript array A REST / JSON API A CSV file JSON lines Open a file of your own…

Every way in. grid.rows = array for objects you already have; <bmx-data-grid src="/api/orders"> or grid.load(url) for a REST or JSON API, CSV, TSV or JSON lines (with your own headers through requestInit); grid.load(file) or openFile() for a file the reader chooses or drops; a <table> written inside the element (below); dataProvider for your server a block at a time, with the sort, filters and search (below); live-src for a WebSocket or server-sent events feed (below); and tree-field or parent-field for tree data (below). No adapter, no plug-in, and the same grid in every case.

Get the code
<script src="/assets/bmx-components.min.js"></script>

<bmx-data-grid id="gr-sources" label="Loaded data" group-panel="false" style="--bmx-data-grid-height: 22rem"></bmx-data-grid>

<script>
  const dataGrid = document.getElementById('gr-sources');
  dataGrid.rows = [ /* your rows */ ];
</script>

An order book, twenty thousand rows

A year of orders made up in this page. Click a heading to sort and Shift+click to sort by another as well; drag a heading onto the bar above the grid to group by it; open a heading's menu to filter by its values, pin it, hide it or fit it. Type in the search. Double-click a cell, or just start typing, to change it; drag across cells, or use Shift and the arrows, to see their total at the bottom.

Modern Striped Bordered Minimal Glass Cards
Group by region and status Stop grouping Overdue and not shipped Clear filters
Keyboard: the arrow keys move the active cell, Ctrl with an arrow to the edge, Shift with an arrow to choose a range, Page Up and Page Down by a page. Enter or F2 edits, typing replaces, Esc cancels, Tab moves on. Space chooses the row or ticks a box, Delete clears the range, Ctrl+C and Ctrl+V copy and paste with any spreadsheet, Ctrl+Z undoes. Above the first row are the headings: Enter sorts, Alt+↓ opens the column's menu, Alt+Shift with an arrow moves the column, Ctrl+Shift with an arrow resizes it. On a group row, the left and right arrows fold and unfold.

One tag, rows in and changes out. grid.columns = [{ field: 'total', type: 'currency', currency: 'GBP', aggregate: 'sum' }, ...] and grid.rows = orders. Edits are written into your own objects, and await grid.getChanges() gives what was added, changed and removed since loading — send it to your server and call acceptChanges(). The view the reader arranged is kept in this browser under state-key; getState() gives it to you to keep anywhere else. After a save, Ctrl+Z still steps back, and what it undoes is reported for saving again. The printer in the toolbar prints every row the filters keep, headings on each page; the Excel file keeps the grid's colours, groups as outline levels and the headings frozen.

Get the code
<script src="/assets/bmx-components.min.js"></script>

<bmx-data-grid id="gr-orders" label="Orders" checkboxes row-numbers state-key="showcase-orders" file-name="orders" style="--bmx-data-grid-height: 38rem"></bmx-data-grid>

<script>
  const dataGrid = document.getElementById('gr-orders');
  dataGrid.columns = [ /* your columns */ ];
  dataGrid.rows = [ /* your rows */ ];
</script>

Fifty column types

Every type on one grid, grouped under headings: text, links, numbers, dates, choices, visual data, media and actions. Scroll sideways; the name column stays pinned. Sort any column to see it sorted by value — priorities from critical down, statuses in their workflow's order — and open a column's menu to see the filter that suits it.

Get the code
<script src="/assets/bmx-components.min.js"></script>

<bmx-data-grid id="gr-types" label="Every column type" toolbar="false" group-panel="false" style="--bmx-data-grid-height: 30rem"></bmx-data-grid>

<script>
  const dataGrid = document.getElementById('gr-types');
  dataGrid.columns = [ /* your columns */ ];
  dataGrid.rows = [ /* your rows */ ];
</script>

Edit, validate, undo, save

A small stock list with rules: every product needs a name and a code of three letters and four digits, stock cannot go below zero, and the page itself refuses a price under cost. Try breaking them. Changed cells are marked in the corner until the changes are saved; paste a block from a spreadsheet, or add a row with the + in the toolbar.

Save changes Put everything back Read only

Your rules, your say. Columns take required, min, max, pattern, maxLength and a validate function; the editor stays open and says why until the value is acceptable. bmxGridCellChange is cancelable, so your page has the last word on every change — typed, pasted, cleared, undone.

Get the code
<script src="/assets/bmx-components.min.js"></script>

<bmx-data-grid id="gr-stock" label="Stock" group-panel="false" style="--bmx-data-grid-height: 22rem"></bmx-data-grid>

<script>
  const dataGrid = document.getElementById('gr-stock');
  dataGrid.columns = [ /* your columns */ ];
  dataGrid.rows = [ /* your rows */ ];
</script>

Trees: an organisation and its budget

Rows that hold rows. Each row here names its parent's key (parent-field); data that carries its children in an array takes tree-field instead. Click a chevron, or use the right and left arrows on a name. Search for a team: it is shown where it lives, with every level above it opened. Sort any column and each level sorts among itself.

Get the code
<script src="/assets/bmx-components.min.js"></script>

<bmx-data-grid id="gr-tree" label="Organisation" parent-field="parent" style="--bmx-data-grid-height: 24rem"></bmx-data-grid>

<script>
  const dataGrid = document.getElementById('gr-tree');
  dataGrid.columns = [ /* your columns */ ];
  dataGrid.rows = [ /* your rows */ ];
</script>

Master and detail

Every order opens to show its lines, in a grid of their own. Click the expander, or press Space on it, and Enter on the panel to move into it. The panel keeps what it holds while you scroll away and back. Give detailRenderer a function instead and the panel holds anything you like — a chart, a form, another component.

Get the code
<script src="/assets/bmx-components.min.js"></script>

<bmx-data-grid id="gr-detail" label="Orders with their lines" group-panel="false" detail-height="230" style="--bmx-data-grid-height: 28rem"></bmx-data-grid>

<script>
  const dataGrid = document.getElementById('gr-detail');
  dataGrid.columns = [ /* your columns */ ];
  dataGrid.rows = [ /* your rows */ ];
</script>

Pivot: rows into totals

A year of sales, pivoted. Press Pivot in the toolbar to choose your own rows, columns and totals; the search and filters still apply to the sales beneath. What comes out is an ordinary grid: sort it, export it, print it.

Region by quarter Product by month The sales as they are
Get the code
<script src="/assets/bmx-components.min.js"></script>

<bmx-data-grid id="gr-pivot" label="Sales" allow-pivot group-panel="false" file-name="sales" style="--bmx-data-grid-height: 26rem"></bmx-data-grid>

<script>
  const dataGrid = document.getElementById('gr-pivot');
  dataGrid.columns = [ /* your columns */ ];
  dataGrid.rows = [ /* your rows */ ];
</script>

A plan you can work in

A launch plan that behaves like a spreadsheet. Choose two cells and drag the square at their corner: numbers, dates and Week 3 carry on. Drag a row by its handle, or press Alt+Shift with an arrow, to move it. Right-click a cell, or press Shift+F10, for its commands. The milestone stays pinned at the top; late tasks turn red and finished ones are struck through, by rules written as data.

Rules as data. format-rules='[{ "field": "due", "when": { "op": "lt", "value": "today" }, "style": { "tone": "danger" } }]' — the filters' own tests, so a rule can live in a database or a server page and read the way the filter that finds the same rows would. Cells can also span: a column's colSpan(row) joins a cell to the ones after it, as the milestone's title does here.

Get the code
<script src="/assets/bmx-components.min.js"></script>

<bmx-data-grid id="gr-plan" label="Launch plan" row-reorder pinned-rows="M1" group-panel="false" style="--bmx-data-grid-height: 24rem"></bmx-data-grid>

<script>
  const dataGrid = document.getElementById('gr-plan');
  dataGrid.columns = [ /* your columns */ ];
  dataGrid.rows = [ /* your rows */ ];
</script>

Cards on a phone, pages when you want them

Narrower than 600 pixels, the grid shows each row as a card with the fields you choose, and it happens by itself; here you can switch. And for readers who prefer pages to one long scroll, pagination adds a pager that the sort, filters and search all respect.

Table Cards Pages
Get the code
<script src="/assets/bmx-components.min.js"></script>

<bmx-data-grid id="gr-customers" label="Customers" pagination page-size="10" card-fields="name company status spend renews" group-panel="false" style="--bmx-data-grid-height: 30rem; max-width: 60rem"></bmx-data-grid>

<script>
  const dataGrid = document.getElementById('gr-customers');
  dataGrid.columns = [ /* your columns */ ];
  dataGrid.rows = [ /* your rows */ ];
</script>

Live prices

Point live-src at a WebSocket or server-sent events URL and each message updates the rows: an array of rows, matched by rowKey, or { "op": "remove", "keys": [...] }. A feed of your own goes through applyLiveMessage(), as this made-up one does. Each changed cell flashes: green up, red down. Sorting, filters and totals keep up, and the connection comes back by itself when it drops.

Prices moving
Get the code
<script src="/assets/bmx-components.min.js"></script>

<bmx-data-grid id="gr-live" label="Prices" readonly toolbar="false" group-panel="false" status-bar="false" appearance="minimal" style="--bmx-data-grid-height: 22rem"></bmx-data-grid>

<script>
  const dataGrid = document.getElementById('gr-live');
  dataGrid.columns = [ /* your columns */ ];
  dataGrid.rows = [ /* your rows */ ];
</script>

A million rows from a server

Some data is too big to send to the page. Here dataProvider is a stand-in server that takes a quarter of a second to answer: the grid asks it for a hundred rows at a time as you scroll, with the sort, filters and search, and shows the rows still on their way as placeholders. Drag the scrollbar to the middle and watch only the rows you land on being asked for.

Wiring it to your API. grid.dataProvider = async ({ start, count, sort, filters, search, signal }) => (await fetch(url(start, count, sort, filters, search), { signal })).json() — answer { rows, total }. A request the reader has scrolled past is aborted. This stand-in sorts by the transaction number and searches by reference; yours applies all of it.

Get the code
<script src="/assets/bmx-components.min.js"></script>

<bmx-data-grid id="gr-server" label="Transactions from the server" readonly group-panel="false" style="--bmx-data-grid-height: 26rem"></bmx-data-grid>

<script>
  const dataGrid = document.getElementById('gr-server');
  dataGrid.columns = [ /* your columns */ ];
  dataGrid.dataProvider = async ({ start, count, sort, filters, search }) => { /* one block from your server */ };
</script>

A million rows in the page

Press the button to give the grid a million rows at once. Scroll, sort by any column, search and group: the rows are never copied, sorting ranks each column's values once, and only what is in view is drawn.

Load a million rows
Get the code
<script src="/assets/bmx-components.min.js"></script>

<bmx-data-grid id="gr-large" label="A million rows" group-panel="false" empty-text="Press the button to load a million rows." style="--bmx-data-grid-height: 26rem"></bmx-data-grid>

<script>
  const dataGrid = document.getElementById('gr-large');
  dataGrid.columns = [ /* your columns */ ];
  dataGrid.rows = [ /* your rows */ ];
</script>

A plain HTML table, made a grid

Wrap a table your server already renders. The grid reads its headings and cells once — data-type on a heading says what the column is, data-value on a cell gives the value behind formatted text — and where script never runs, the table is still a table.

OfficeStaffRevenueOpenedStatus
York42$1.84mMarch 2019Open
Leeds118$6.12mSeptember 2014Open
Manchester86$4.41mJanuary 2016Open
Bristol0—February 2027Planned
Get the code
<script src="/assets/bmx-components.min.js"></script>

<bmx-data-grid id="gr-table" label="Offices" toolbar="false" group-panel="false" status-bar="false" style="--bmx-data-grid-height: 17rem">
  <table>
    <thead>
      <tr><th>Office</th><th data-type="integer">Staff</th><th data-type="currency">Revenue</th><th data-type="date">Opened</th><th data-type="status">Status</th></tr>
    </thead>
    <tbody>
      <tr><td>York</td><td>42</td><td data-value="1840000">$1.84m</td><td data-value="2019-03-04">March 2019</td><td>Open</td></tr>
      <tr><td>Leeds</td><td>118</td><td data-value="6120000">$6.12m</td><td data-value="2014-09-15">September 2014</td><td>Open</td></tr>
      <tr><td>Manchester</td><td>86</td><td data-value="4410000">$4.41m</td><td data-value="2016-01-11">January 2016</td><td>Open</td></tr>
      <tr><td>Bristol</td><td>0</td><td data-value="0">&mdash;</td><td data-value="2027-02-01">February 2027</td><td>Planned</td></tr>
    </tbody>
  </table>
</bmx-data-grid>