A GridStack alternative for dashboards that outgrow the grid
Honest framing first: GridStack.js is the reference implementation of drag-resize-repack, it's MIT, and if a layout engine is all you need, use it. Grafloria's dashboard kit is for the moment the grid becomes an application: widgets with data, undo across every gesture, containers inside containers, exports, and a save format shared with your diagrams.
The comparison here is with grid layout engines. Where a BI product such as DevExpress Dashboard comes up below, it is only for how its designer aligns widgets — a splitter surface that is always covered — which the kit's split layout reproduces. Data sources, query builders and the rest of a BI suite are out of scope on both sides of this table.
When to stay with GridStack
You need exactly a grid: place, drag, resize, re-pack, responsive breakpoints, sub-grids. GridStack does all of it, small and focused, with wrappers for Angular, React and Vue 3.
Feature by feature
✓ built in · DIY possible, but yours to build · — out of scope. Both columns are meant to be true, including the rows we lose.
| GridStack.js | Grafloria dashboard kit | |
|---|---|---|
| Grid & layout | ||
| Drag / resize / re-pack | ✓ — the reference implementation | ✓ push-down with skip, swap shapes, truthful placeholder |
| Nested grids | ✓ subGridOpts (geometry) | ✓ containers — cross-boundary drag with adoption, one undo step |
| Responsive columns | ✓ breakpoints | ✓ cells re-flow, not just clamp |
| Locked tiles | ✓ locked/noMove/noResize | ✓ pinned, movable, resizable — pinned survives every reflow |
| Fluid width — 100% of the container | ✓ | ✓ the default; mode: 'fixed' keeps an authored world for a board inside a diagram |
| Per-widget min / max size | ✓ minW … maxH | ✓ limits: { minSpan, maxSpan, minRows, maxRows } |
| Resize from any edge | ✓ configurable handles | ✓ every edge and corner, the cursor says which |
| Static (read-only) grid | ✓ staticGrid | ✓ static: true, switched live with setStatic() |
| Drag handle | ✓ handle selector | ✓ dragHandle — the caption strip, your own selector, or a painted grip (left / center / right, in the header or a tab above the card) shown on the selected widget only; switched live |
| Sizing modes | ✓ cellHeight, maxRow | ✓ sizing: 'grow' — rows keep their height, the board extends; 'fit' — rows squeeze, the board never scrolls; switched live |
| Splitter layout — the surface is always covered | — | ✓ layout: 'split' — the way a BI dashboard designer (DevExpress's, for one) aligns widgets: one fills, the next halves, dividers are percentages, a drop lands on an edge; switched live, same document |
| Bounded height | ✓ maxRow | ✓ fit mode is a capacity — a board never paints past its own edge |
| Keyboard operation | not documented | ✓ arrows move, Shift+arrows resize, every outcome announced — axe-core in CI |
| Touch | ✓ | ✓ drag and resize, 24-px handles visible without hover |
| Widgets & content | ||
| Content model | content HTML string — rendering is yours | kind + data — widgets are data |
| Built-in renderers (KPI, line, donut…) | — | ✓ six, themed |
| Your own charts | DIY inside the HTML | ✓ renderWidget seam — mount anything |
| Editing | ||
| Undo / redo | — | ✓ every gesture one command, ⌘Z included |
| Cross-container gestures | DIY on top of the geometry | ✓ adoption + escalation, one undo |
| Persistence & sharing | ||
| Save / load | ✓ cell geometry | ✓ the whole board — widgets, nesting, data |
| Export PNG / SVG / PDF | — | ✓ client and server-side |
| Diagrams in the same document | — | ✓ one engine, one format, one undo stack |
| Real-time collaboration | — | ✓ per-property CRDT, transport-agnostic |
| Project | ||
| License | MIT | MIT — no pro tier |
| Dependencies | zero | a handful; ELK layout is a lazy chunk (sizes) |
| Framework wrappers | ✓ Angular · React · Vue | ✓ native components, same three + web component |
| Maturity | ✓ a decade in production, huge install base | newer — every claim above is a CI-tested demo you can open |
The same dashboard, both ways
GridStack builds the grid; the widgets' content is HTML you render yourself:
import { GridStack } from 'gridstack';
const grid = GridStack.init({ column: 12 });
grid.load([
{ x: 0, y: 0, w: 3, h: 1, content: '<div class="kpi">Total revenue</div>' },
{ x: 3, y: 0, w: 8, h: 2, content: '<canvas id="trend"></canvas>' }, // chart wiring is yours
]);
const layout = grid.save(); // geometry back out
Grafloria declares the widgets too — the chart is the declaration:
import { render, dashboard } from '@grafloria/element';
const SPEC = dashboard({
columns: 12,
widgets: [
{ id: 'rev', kind: 'kpi', span: 3, rows: 1,
data: { label: 'Total revenue', value: '$6.81M', delta: 12.4 } },
{ id: 'trend', kind: 'line', span: 8, rows: 2, title: 'Revenue vs target',
data: { series: [{ name: 'Revenue', values: [420,455,512,588,648] }],
labels: ['Jan','Feb','Mar','Apr','May'] } },
],
});
render(SPEC, document.getElementById('canvas'));
// SPEC.handle.toJSON() round-trips the WHOLE board — widgets, nesting, data
How the concepts map
| GridStack.js | Grafloria dashboard kit |
|---|---|
x, y, w, h on an item | x, y, span, rows — or omit them and widgets flow in declaration order |
content HTML string | kind + data — six built-in renderers (KPI, line, donut…), or your own via renderWidget |
grid.save() / grid.load() | toJSON() ↔ dashboard() — one document format shared with diagrams |
subGridOpts nested grids | a widget carrying widgets — cross-boundary drag with adoption, one undo step |
locked / noMove | pinned: true — never pushed, survives every reflow |
column(n), responsive breakpoints | columns + responsive re-flow of the cells themselves |
staticGrid | static: true — the viewer's mode; the API and undo keep working |
handle selector | dragHandle: true | '.sel' | { grip: true, position, placement } — caption strip, your element, or a grip on the selected widget |
| no equivalent | layout: 'split' — a splitter tree beside the grid, setLayout() keeps the picture either way |
minW / maxW / minH / maxH | limits: { minSpan, maxSpan, minRows, maxRows } on the widget |
maxRow | sizing: 'fit' — the board's height is the capacity; overflow: 'scroll' opts out |
See the difference in one demo
The clearest way to judge the boundary: GridStack gives you the grid geometry;
this demo is a dashboard whose
sections are containers — drag a tile across a section boundary and the other grid adopts it
live, one undo restores the whole gesture, and toJSON() serializes the nesting.
The grid options demo shows column
counts, packing and responsive re-flow driven as data, and the
fluid dashboard has Fit / Grow, Grid / Split
and the drag-handle modes in one toolbar.
Pricing and feature cells reflect vendor pages and shipped type definitions as of July–September 2026 — always confirm on the vendor's site. Corrections welcome on GitHub; this page aims to stay honest, not to win.