Skip to content
D
Documentation

Build dashboards

how-to
4 min readUpdated

Declare dashboard views and widgets as data, mount the board in your framework, then switch views or layout and save the live board as a snapshot.

When to use the dashboard kit

Use GrafloriaDashboard (React and Vue) or GrafloriaDashboardComponent (Angular) when people arrange widgets in grid cells. A dashboard is data first: views contain widget specs, and the kit supplies the grid, drag and resize behavior, and undo. The same headless model drives every framework binding. Plain JavaScript builds a spec with dashboard() and mounts it with render(); framework bindings expose a live DashboardHandle, whose snapshot has the DashboardSnapshot shape. Widget declarations use DashboardWidgetSpec.

The built-in widget renderers draw kpi, line, bar, donut, funnel, and table widgets from their data; start with those before supplying custom rendering. The examples below use KPI, line, and donut data.

See the live dashboard builder for a multi-view board with built-in widgets. The demo source shows its full builder UI.

Declare and mount the board

Each view has an id and a widgets array; each widget needs an id and can name a renderer kind, cell span and rows, and renderer data. With no explicit x and y, widgets flow in declaration order. span defaults to 3 columns and rows to 1. The framework components mount the board from these specs; give their host a height so the rendered dashboard has room.

The following examples declare two real views and mount the same board in each supported framework. The initial board displays KPI cards and a donut in Overview; the Revenue view has its own line and KPI widgets.

js
import { dashboard, render } from '@grafloria/element';

const root = document.createElement('main');
const nav = document.createElement('nav');
const overviewButton = document.createElement('button');
overviewButton.textContent = 'Overview';
const revenueButton = document.createElement('button');
revenueButton.textContent = 'Revenue';
const layoutButton = document.createElement('button');
layoutButton.textContent = 'Switch layout';
const saveButton = document.createElement('button');
saveButton.textContent = 'Save board';
nav.append(overviewButton, revenueButton, layoutButton, saveButton);
const canvas = document.createElement('div');
canvas.style.height = '560px';
root.append(nav, canvas);
document.body.append(root);

const views = [
  { id: 'overview', name: 'Overview', widgets: [
    { id: 'sales-kpi', kind: 'kpi', span: 3, rows: 1,
      data: { label: 'Sales', value: '$6.8M', delta: 12.4, spark: [4.2, 4.5, 5.1, 6.8] } },
    { id: 'customers-kpi', kind: 'kpi', span: 3, rows: 1,
      data: { label: 'Customers', value: '1,284', delta: 8.1, spark: [980, 1090, 1150, 1284] } },
    { id: 'region-mix', kind: 'donut', span: 6, rows: 2, title: 'Sales by region',
      data: { slices: [{ label: 'EMEA', value: 2.9 }, { label: 'Americas', value: 2.4 }, { label: 'APAC', value: 1.5 }] } },
  ] },
  { id: 'revenue', name: 'Revenue', widgets: [
    { id: 'revenue-line', kind: 'line', span: 8, rows: 2, title: 'Revenue trend',
      data: { series: [{ name: 'Revenue', values: [4.2, 4.5, 5.1, 6.8] }], labels: ['Jan', 'Feb', 'Mar', 'Apr'] } },
    { id: 'revenue-kpi', kind: 'kpi', span: 4, rows: 1,
      data: { label: 'Quarter total', value: '$6.8M', delta: 12.4 } },
  ] },
];

const saved = localStorage.getItem('sales-dashboard');
const spec = dashboard(saved ? JSON.parse(saved) : { columns: 12, views });
const instance = render(spec, canvas);
const handle = spec.handle;
overviewButton.addEventListener('click', () => handle.showView('overview'));
revenueButton.addEventListener('click', () => handle.showView('revenue'));
layoutButton.addEventListener('click', () => {
  const next = handle.getLayout() === 'grid' ? 'split' : 'grid';
  handle.setLayout(next);
});
saveButton.addEventListener('click', () => {
  localStorage.setItem('sales-dashboard', JSON.stringify(handle.toJSON()));
});
window.addEventListener('pagehide', () => instance.dispose(), { once: true });

The JavaScript, Angular, React, and Vue examples mount with Overview visible. Their built-in painters render the KPI, donut, and line cards from their data, and the board fits them into grid cells. Choose Revenue to frame the other view; Switch layout toggles the board between the cell grid and split layout. Save board stores the live snapshot in localStorage.

Switch views and layout

The framework components keep view selection in their activeView prop or model. Their layout prop is a live switch: changing it applies a handle call without remounting. For JavaScript, get the live handle from the dashboard spec and call showView(id) or setLayout('grid' | 'split') directly. In every binding, choose a declared view id; DashboardHandle exposes views in declaration order and activeView as the current id.

The JavaScript sample uses dashboard() to create the render spec, then render() to mount it. spec.handle is the mounted DashboardHandle: setLayout() switches the active view between grid and split, while showView() frames the requested view.

Persist the live board

Call toJSON() on the handle after edits to capture the live board as plain data, then serialize that snapshot with your application's storage. The snapshot includes the current view layouts and board options; it excludes function callbacks. Restore it by passing the saved data back into dashboard() in JavaScript. Supply any non-serializable rendering callbacks again when rebuilding. In Angular, snapshot() returns the same data as toJSON().

The JavaScript, Angular, React, and Vue examples demonstrate saving to browser localStorage. Use storage appropriate to your application when the board must survive a browser change or be shared between users. A saved snapshot has the DashboardSnapshot shape.

Options that shape the board

Pass board geometry and behavior through options. A view can also override its column count. Widget span and rows describe its cell size, while optional x and y place it explicitly.

OptionTypeDefaultWhat it does
columnsnumber12Sets the column count for every view unless a view overrides it.
gapnumber8Sets the gap between widgets and the board padding, in pixels.
sizing'fit' | 'grow''grow' on fluid boards; 'fit' on fixed boardsgrow keeps row height and extends the board; fit keeps the board height and squeezes rows.
layout'grid' | 'split''grid'Chooses a cell grid or a splitter tree. Switch it live with the handle or component prop.
rowHeightnumber130Sets row height in grow mode, in pixels.
floatbooleanfalseWith false, gravity packs widgets upward; with true, gaps can remain where widgets are dropped.
width, heightnumber1180 × 660Set a fixed board size. An explicit width selects fixed mode.
staticbooleanfalseTurns off pointer dragging, resizing, and handles for a viewer board.

On DashboardWidgetSpec, id identifies a widget, kind selects its renderer, data carries its payload, and title supplies a title. pinned: true prevents reflow from moving that widget. The six built-in kinds need no custom renderer; unknown kinds use a titled placeholder frame.

Pitfalls

  • Give the dashboard host a resolved height; otherwise its canvas has no space to draw into.
  • views and the single-view widgets shorthand are mutually exclusive. Use views when people switch between boards.
  • Framework wrappers mount the board once; changing data or options afterward does not rebuild it. Use live props for activeView, layout, and sizing, and use the handle for live board operations.
  • React custom widgets use widgetTypes, not options.renderWidget; Vue uses widget-<kind> slots and Angular uses grafloriaWidget templates. This page uses built-in renderers instead.

Live demo

Open the dashboard builder to see its tabs, built-in widget cards, and dashboard layout. Read the demo source for the full palette and persistence controls.

Was this page helpful?

Build dashboards — Grafloria · GPT-6 Luna