v0.9.1 · stable
Elyra DataGrid · Data grid

Ten million rows.
Thirty milliseconds.

The data grid is the most demanding component you ship. Elyra DataGrid is the only one that is server-driven, scales to millions of rows, works across Livewire, Vue and Svelte, and follows your Tailwind theme — all at once. One protocol, three clients, one PHP server.

Elyra DataGrid showing 10 million sales rows with faceted filtering, grouping with rollup subtotals and hybrid search
Illustrative only — one example DataGrid configuration.
Live demo

Not a screenshot.
the real engine.

This grid runs the exact client-core engine that powers the Livewire, Vue and Svelte clients — here against 2,400 rows in your browser. Sort a column, search, filter by region, page through. In production the same engine talks to the PHP server and only ever queries a viewport, so ten million rows feel identical.

Sales in-browser · 2,400 rows
Page 1 / 1 0 matches · ready

Sample data, generated in-browser. The production grid is server-driven over the DataGrid protocol.

This is a deliberately minimal setup — just sorting, search, a filter and paging. The same grid configures a lot more, each an opt-in flag on the definition:

See the grid definition guide for the full feature surface.

Why it's different

Grids force a compromise.
This one doesn't.

Client-bound grids fall over past a few thousand rows. Ecosystem-locked grids marry you to one framework. Headless cores leave the server, filtering UI, editing and export as homework. Elyra DataGrid makes four decisions instead.

01

One protocol, many clients

A small JSON contract — GridRequest / GridResponse — generates both the TypeScript types and the PHP DTOs from one source. The Livewire, Vue and Svelte clients are genuinely interchangeable and always in sync. You are never locked into one frontend.

02

Server-driven by design

Sorting, filtering, grouping, aggregation, search and paging run in the database, never in JavaScript. The client renders only a viewport — so the grid behaves the same over 100 rows and 10,000,000. The work is O(viewport), not O(data).

03

Native to ElyraSQL, portable everywhere

The server turns database features straight into grid features: FACET value lists with counts, per-level subtotals, percentile/median footers, HYBRID semantic + full-text search, and streamed cursor exports of millions of rows at constant memory. Dialect drivers mean it also runs on MySQL, PostgreSQL, ClickHouse, and SQLite / Elyra SQL Anywhere. Since 0.8.0 the dialect follows the connection: a grid on PostgreSQL without ->driver('pgsql') used to get ElyraSQL's backtick quoting and fail on its first query, and the PostgreSQL suite never saw it because every one of its grids named the driver. An explicit ->driver() still wins, MySQL and MariaDB stay on the ElyraSQL dialect, and every case now also runs on MySQL 8.4 (700 cases, was 466), where fulltext search needs a FULLTEXT index over exactly the searched columns and fails with error 1191 without one.

04

Follows your app

A Tailwind v4-native theme that inherits your palette, accent and dark mode automatically — it looks like part of your app on day one, not a bolted-on widget.

One grid, your stack

Pick the client. Keep the grid.

The same features, keyboard spec and server — three first-class clients that share a headless TypeScript core and one PHP package.

Server-driven
Livewire + Flux UI

Grid state lives in the component; the protocol request is built and answered during render. Drop it into a Blade view — no build step, no API endpoint to wire. It did not compile under Livewire 4 at all in 0.4.0 or 0.5.0: Livewire 4 injects a PHP block into the opening tag of a looped element carrying wire:key, and Blade's component-tag compiler cannot parse an opening tag containing PHP. Nothing caught it because no test had ever rendered the component — orchestra/testbench and phpunit were dev dependencies and entirely unused. There is a Testbench harness now, and it runs in CI. It had a blind spot of its own: every test ran from the monorepo, and until 0.9.1 the theme loader read a directory that exists only there, so on every install the page got an empty style tag and the grid rendered unstyled — the no-build-step theming had never worked for anyone who installed the package. The theme now ships inside the package, and test:archive unpacks the package as a customer receives it and asks the installed provider for its CSS, which reported 0 bytes against 0.9.0. Since 0.9.0 it needs PHP 8.3, Laravel 13.30, Livewire 4.3.4 and Flux 2.12.2: Laravel and Livewire at the first releases with no known security advisory, so the package cannot be paired with a vulnerable framework; Flux at the first that allows Laravel 13; and PHP 8.3 because Laravel 13 requires it. 0.8.x stays the line for Laravel 10 to 12 and PHP 8.2. Livewire 3 was dropped in 0.8.0, its ^3.5 promise never having been tested. And the browser can no longer rewrite the grid: the package's own public properties are an allow-list, so public bool $exportable = false; holds even against a $wire.set(), and it is a Livewire trait hook rather than #[Locked] because PHP does not inherit attributes onto a redeclared property, which is exactly the case the lock exists for.

Client-driven
Vue 3 + Inertia

A headless engine holds state and POSTs the protocol request to a Laravel endpoint, with SSR-seeded first paint so the first screen is instant.

Client-driven
Svelte 5 + Inertia

The same headless core with Svelte 5 runes — virtual rows and columns, the full keyboard spec, and your Tailwind theme, on the Inertia stack.

On 0.9.0 or earlier? Upgrade to 0.9.1 — the Livewire grid rendered unstyled on every install, and paging past the limit returned the wrong rows.

The Livewire grid rendered unstyled on every install, in every release up to 0.9.0. The theme loader read a directory that exists in the development monorepo and nowhere else; installed, it pointed into vendor, every file check failed silently, and each page got an empty <style></style>. Every test passed because every test ran from the monorepo. The theme now ships inside elyra/datagrid-livewire, and a missing one logs a warning instead of staying silent. If you wrote a stand-in stylesheet, or imported the CSS from elyra/datagrid-js as a workaround, you can remove it.

Paging past maxSkip returned the wrong rows, and said they were the right ones. The offset was clamped, so a request for row 150,000 on the default 100,000 limit got the rows at 100,000 while the response still reported skip: 150000, and every client placed them where it had asked. A window past the limit now returns no rows and a pagingLimit field; all three clients say the limit was reached, and the Livewire pager no longer offers pages it cannot fill.

Qualified table names failed on every driver. reporting.sales was quoted as one identifier, so the database looked for a table literally called that — reported against ClickHouse, but true of MySQL, PostgreSQL and SQLite too, and 0.9.0 made it worse for the re-read after a mutation. Tables are now quoted part by part. 0.9.1 needs what 0.9.0 does: Laravel 13.30 and PHP 8.3.

Also: on 0.7.x or earlier with the Livewire client? Upgrade to 0.8.0 or later — the browser could switch back on what you had switched off. Every public Livewire property is writable from the browser unless something refuses it, and nothing did. On a grid with public bool $exportable = false; that relied on the route for authorization, one $wire.set() re-enabled export and the next call downloaded the table. The same applied to $assistant — billed LLM calls on a grid that never opted in — to the pivot layout, which bypassed dimensions(), and to the queued-export id and the cached totals. The package’s own properties are now an allow-list: the browser may write only what the views bind. The server package and the Vue and Svelte clients do not depend on Livewire and are unchanged.

Before you upgrade, three things can affect an existing app. Your own tests that call ->set('assistant', true) now throw, because set() is what the browser does — pass configuration at mount instead. A custom view that binds a package property with wire:model has to open it explicitly. 0.8.0 needs Livewire 4, Flux 2.6.1 and Laravel 10.48; 0.9.1, the latest, needs PHP 8.3, Laravel 13.30, Livewire 4.3.4 and Flux 2.12.2, so on Laravel 10 to 12 or PHP 8.2 take 0.8.x, which has the same fix. And without ->driver() the dialect now follows the connection: a grid on PostgreSQL used to fail on its first query, and SQLite and ClickHouse get their own dialects too — so a SQLite grid that relied on ElyraSQL’s aggregates or search should name ->driver('elyrasql') to keep them. The upgrade guide has each of them with the exact change.

Also: on Livewire below 4.3.4 or Laravel below 13.30? Those releases have published XSS advisories — CVE-2026-81887 in Livewire’s client-side state handling, CVE-2026-102279 in Laravel’s debug page — and they are not DataGrid’s own. Since 0.9.0 the package’s floors sit at the first releases without them, so it will not install beside a framework that has them.

AI · Natural language

Ask the Grid.
in plain language.

Let anyone query millions of rows without knowing your columns or SQL. They type a question — “revenue over 50k in region Nord last quarter” — and the grid turns it into a real filter.

  • Server-side, constrained. An LLM whose output can only ever use this grid's declared columns and operators — nothing else.
  • Re-validated, then compiled. The suggestion is checked against the allow-list and run through the ordinary FilterCompiler — columns allow-listed, values bound.
  • Never unsafe SQL. An off-target guess degrades to dropping that one condition — it can never produce unsafe SQL.
  • All three clients. Livewire, Vue and Svelte — add an ask box and go.
Sales · 10,000,000 rows
revenue over 50k in region Nord last quarter ask
region = Nord revenue > 50 000 date ≥ 2026-04-01 date < 2026-07-01
✓ validated against allow-list · compiled by FilterCompiler · ~30 ms
Measured, not claimed

Fast at any size —
because size stops mattering.

Benchmarked against a 10,000,000-row table on ElyraSQL 1.4.7. Interactive operations — paging, sorting, filtering — stay flat because the grid only ever queries a viewport.

See the full benchmark
~30 ms
Viewport page (20 rows)
~30 ms
Page within a filter
~29 ms
Sort by an indexed column
10M
Rows, streamed export at constant memory

Everything a grid needs. Nothing left as homework.

Sort & filter

Multi-column sort and per-column filters, plus an advanced AND/OR filter builder — all resolved in the database.

Faceted filtering

Filter-by-checkbox value lists with live counts (FACET), computed lazily — only when a filter panel opens.

Ask the grid (natural language)

Users type a question — “revenue over 50k in region Nord” — and a server-side LLM, constrained to the grid’s own columns and operators and re-validated against the allow-list, compiles it exactly like a hand-built filter. Never unsafe SQL; works across all three clients. The safety of the compiled filter was never the gap — the spending was. Until 0.5.0 the config flag meant to switch the assistant off was never read by anything, so ask() was reachable on any grid page with the interface merely hidden, with no length bound and no rate limit on a billed model call. It is a real server-side kill switch now, questions cap at 500 characters, and calls are metered per caller and grid with a limiter that fails closed. 0.7.0 makes the answer explainable, and closes two ways it was quietly wrong. The allow-list has always dropped conditions it cannot permit, and doing so silently left the user with a filter answering a narrower question than they asked; interpret() now reports what was thrown away, and all three clients show it. Deliberately not a model-reported confidence score — a self-reported confidence is not calibrated, and rendering 92% confident invites trust at exactly the moment the filter should be checked, where what was dropped is ground truth. The two wrong answers: a question about a text column holding 00123 bound the integer 123 and matched nothing, a silently empty grid rather than an error, and a half-open between compiled to BETWEEN ? AND NULL and matched no rows — values are coerced by the declared type now and the half-open range is refused and reported. The reverse direction ships too: explain() renders the filter in force as a sentence under the toolbar, and it is not an LLM call, because a filter tree is structured data and describing it is formatting rather than inference — faithful by construction, free, and therefore on every grid rather than only where the assistant is switched on. Switched on by the host, and only by the host: until 0.8.0 a browser could set $assistant to true on a grid that never opted in and run billed calls.

Grouping & aggregates

Group by any column with subtotals, non-additive aggregates like median/percentile, and expandable detail rows. This page said WITH ROLLUP until 0.6.0 and that was never how it worked: the rollup flag was documented as the way to get subtotals, plumbed from all three clients through the protocol into the driver contract, and never read by the engine — groupBy was always called with false. Nothing was lost, because the subtotals were never produced by ROLLUP anyway. The engine runs one GROUP BY per level, so each level aggregates over its own rows, which is what makes a median subtotal correct rather than a mean of the cells above it. The dead surface is removed in 0.6.0. Pivot mode arrived in the same release, in all three clients, and rests on exactly that: every subtotal and margin is its own aggregate query rather than a sum of the visible cells, which is the difference between a correct median subtotal and a plausible one. It costs one query per grouping level, issued in parallel — four for a 1×1 pivot — and grouping queries finally have a limit at all, since the number of group rows scales with a column's distinct values rather than the page size. A truncated grouping says so in the response instead of being presented as complete. On PostgreSQL it did not work at all before 0.5.0 — the engine quoted every identifier through the driver except one hardcoded backtick, so a headline feature raised a syntax error on a database this page calls supported. The live PostgreSQL suite never loaded the engine, which is why it survived; there is an engine-level suite now that drives the real thing against a real server.

Paging & virtual scroll

Classic paging or smooth virtual scrolling — rows and columns virtualized — across all three clients.

Editing

Inline, in-cell, modal or slideover editing, with batch edits — validated on the server. Two things 0.5.0 closed that a page selling editing has to say. With any row pinned, paste and copy addressed the wrong record: each client sorted its own copy for display while the core resolved the active cell against the unsorted array, so it was a silent wrong-row UPDATE rather than a wrong paste. And create and update returned the row with SELECT *, handing the client every column in the table — password hashes, tokens, internal flags — where it now returns only the columns the grid declares. Since 0.7.0 an applied mutation fires GridRowChanged with the row before and after over those same declared columns — the boundary the response already respects, so an audit trail cannot leak what the grid does not show — and inverse() gives the mutation that would undo it. For a delete it returns null on purpose: restoring the row would pass through the write allow-list, which strips the non-editable columns including the key, so the restored row would come back with a new id and missing data. An undo that quietly produces a different row is worse than one that admits it cannot be built. Capturing the previous row costs one SELECT, so it happens only when something is listening.

Selection & keyboard

Row and range selection with a complete keyboard spec and clipboard support — spreadsheet-grade navigation.

Master-detail & tree

Expandable master-detail rows and full tree-grid hierarchies, lazily loaded from the server.

Frozen & auto-fit columns

Freeze columns left and right, auto-fit widths, reorder and resize — with column virtualization for very wide grids.

Streamed export

Streamed CSV and rich XLSX of the full result set — millions of rows — from a server cursor at constant memory. That was not true of a grid with no key until 0.5.0: such a grid cannot be paginated, so the export buffered the whole result set, contradicting its own docblock while max_export_rows defaulted to null. It refuses without an explicit cap now rather than risking an out-of-memory. The other limit is the connection rather than the memory: a streamed export holds an HTTP response open for its whole duration, and the 1.67M-row benchmark takes about 52 seconds against a 60-second ALB or nginx idle timeout, occupying a PHP-FPM worker throughout. 0.6.0 adds a queued export — progress in the toolbar, then a signed download link — with four gates on that link, because a signed URL is a bearer capability and Laravel's signed URLs do not authenticate: finished, unexpired, owned by the caller, and still permitted by the grid's export policy re-evaluated at download time. The ElyraSQL key-window strategy also walked the key's value range, so Snowflake or ULID-style ids needed around a trillion queries — bounded now, with sparse keys falling back to keyset paging: two queries where there were 1.2e12.

Hybrid search

A search box that understands meaning, not just substrings — semantic + full-text (HYBRID) via ElyraSQL.

Themed for your app

Tailwind v4-native theming that inherits your palette, accent and dark mode with no configuration.

Localization & state

Localizable UI and persistent grid state (columns, sort, filters) across sessions.

Pricing

A commercial license,
for a grid you won't outgrow.

Not open source — every plan ships the full source of all packages (protocol, PHP server, headless core, all three clients and the theme), updates and support. Free to evaluate on local and development domains; a license activates your production domains. One purchase covers Livewire, Vue and Svelte.

Single
$49 / year
1 production domain
Popular
Team
$199 / year
5 production domains
Unlimited
$650 / year
Unlimited domains
Limited
Lifetime
$999 one-time
Unlimited domains, forever

All annual plans renew yearly and include updates + support for the term. The Lifetime plan is a one-time purchase with perpetual updates — a limited launch offer. See the licensing docs for details.

Elyra DataGrid · v0.9.1 Built in Norway · MMXXVI