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.jsGrafloria 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 operationnot 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 modelcontent HTML string — rendering is yourskind + data — widgets are data
Built-in renderers (KPI, line, donut…)—✓ six, themed
Your own chartsDIY inside the HTML✓ renderWidget seam — mount anything
Editing
Undo / redo—✓ every gesture one command, ⌘Z included
Cross-container gesturesDIY 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
LicenseMITMIT — no pro tier
Dependencieszeroa 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 basenewer — 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.jsGrafloria dashboard kit
x, y, w, h on an itemx, y, span, rows — or omit them and widgets flow in declaration order
content HTML stringkind + 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 gridsa widget carrying widgets — cross-boundary drag with adoption, one undo step
locked / noMovepinned: true — never pushed, survives every reflow
column(n), responsive breakpointscolumns + responsive re-flow of the cells themselves
staticGridstatic: true — the viewer's mode; the API and undo keep working
handle selectordragHandle: true | '.sel' | { grip: true, position, placement } — caption strip, your element, or a grip on the selected widget
no equivalentlayout: 'split' — a splitter tree beside the grid, setLayout() keeps the picture either way
minW / maxW / minH / maxHlimits: { minSpan, maxSpan, minRows, maxRows } on the widget
maxRowsizing: '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.