Blog / Use cases
A dashboard is three problems wearing one grid
Every dashboard project starts the same way: you find a grid library, the drag-and-drop demo works in an afternoon, everyone's happy. Three sprints later you're maintaining a small distributed system. Not because the grid was bad — because the grid was only ever one third of the problem.
The three problems
- Geometry. Place, drag, resize, re-pack, don't jitter. Grid libraries — GridStack especially — genuinely solve this. It's the part that demos well.
- Widget state. Each tile has a kind, a config, and data. The grid doesn't know any of that, so you build a registry mapping cells to widgets, and now every add/remove has to update two structures that can disagree.
- Persistence & history. Layouts must survive reload; users expect ⌘Z after a bad drag. Now you're versioning two structures and synthesizing undo across them — and the bug reports read "my widget disappeared", which is what desync looks like from the outside.
If you've built this, none of it is news. The engineering write-ups about dashboard grids all converge on the same sentence: the hard part is keeping visual layout, widget configuration and persistence synchronized while users edit in real time.
What "one document" changes
Grafloria's dashboard kit collapses the three structures into one: the board is a
document. A widget is data (kind + data + a cell), a
gesture is a command on that document, and persistence is the document itself:
import { render, dashboard } from '@grafloria/element';
const spec = dashboard({
columns: 12,
widgets: [
{ id: 'rev', kind: 'kpi', span: 3, data: { label: 'Revenue', value: '$6.8M', delta: 12.4 } },
{ id: 'trend', kind: 'line', span: 9, rows: 2, title: 'Trend', data: { /* your series */ } },
],
});
render(spec, host);
// persistence IS the document — no second structure to keep honest:
const saved = spec.handle.toJSON(); // later: dashboard({ ...saved, renderWidget })
Because every gesture is a command, ⌘Z works across all three layers at once — a cross-container drag that re-packed two grids and grew a section undoes as one step. And because widgets are declared data, "my widget disappeared" stops being a bug class: there is no second registry to fall out of sync.
Sections, since your users will ask
The request that breaks most homegrown dashboards on arrival: "can we group these
KPIs in a box, and drag things in and out of it?" In the kit a container is one field —
a widget carrying widgets nests a full board, with cross-boundary drag and
the section growing when a child needs more room:
{ id: 'kpis', title: 'KPI section', span: 12, columns: 4,
widgets: [
{ id: 'k1', kind: 'kpi', span: 1, data: { label: 'Revenue', value: '$6.8M' } },
{ id: 'k2', kind: 'kpi', span: 1, data: { label: 'Customers', value: '1,284' } },
] }
Your charts stay yours — renderWidget hands you the widget and a raw
element, and the kit never picks a charting library for you.
Play with it live: the dashboard builder and nested containers · the guide: Dashboards in plain JavaScript · the honest boundary with grid libraries: alternatives/gridstack.