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.
jsimport { 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 });
tsimport { Component } from '@angular/core';
import { GrafloriaDashboardComponent } from '@grafloria/angular';
import type { DashboardHandle, DashboardViewSpec } from '@grafloria/element';
@Component({
selector: 'app-sales-dashboard',
standalone: true,
imports: [GrafloriaDashboardComponent],
template: `
<nav>
<button (click)="tab = 'overview'">Overview</button>
<button (click)="tab = 'revenue'">Revenue</button>
<button (click)="switchLayout()">Switch layout</button>
<button (click)="save()">Save board</button>
</nav>
<grafloria-dashboard [views]="views" [options]="options" [(activeView)]="tab"
(ready)="handle = $event" style="display:block; height:560px" />
`,
})
export class SalesDashboardComponent {
tab: string | undefined = 'overview';
handle?: DashboardHandle;
options = { columns: 12, gap: 8 };
views: DashboardViewSpec[] = [
{ 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 } },
] },
];
switchLayout(): void {
const handle = this.handle;
if (handle) handle.setLayout(handle.getLayout() === 'grid' ? 'split' : 'grid');
}
save(): void {
const snapshot = this.handle?.toJSON();
if (snapshot) localStorage.setItem('sales-dashboard', JSON.stringify(snapshot));
}
}
tsximport { useState } from 'react';
import { GrafloriaDashboard } from '@grafloria/react';
import type { DashboardHandle, DashboardViewSpec } from '@grafloria/element';
const views: DashboardViewSpec[] = [
{ 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 } },
] },
];
export default function SalesDashboard() {
const [handle, setHandle] = useState<DashboardHandle>();
const [tab, setTab] = useState('overview');
const [layout, setLayout] = useState<'grid' | 'split'>('grid');
return (
<main>
<nav>
<button onClick={() => setTab('overview')}>Overview</button>
<button onClick={() => setTab('revenue')}>Revenue</button>
<button onClick={() => setLayout(layout === 'grid' ? 'split' : 'grid')}>Switch layout</button>
<button onClick={() => {
if (handle) localStorage.setItem('sales-dashboard', JSON.stringify(handle.toJSON()));
}}>Save board</button>
</nav>
<GrafloriaDashboard views={views} activeView={tab} layout={layout} onReady={setHandle}
style={{ display: 'block', height: 560 }} />
</main>
);
}
vue<script setup lang="ts"> import { ref } from 'vue'; import { GrafloriaDashboard } from '@grafloria/vue'; import type { DashboardHandle, DashboardViewSpec } from '@grafloria/element'; const handle = ref<DashboardHandle | null>(null); const tab = ref('overview'); const layout = ref<'grid' | 'split'>('grid'); const views: DashboardViewSpec[] = [ { 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 } }, ] }, ]; function switchLayout(): void { const current = handle.value; if (current) layout.value = current.getLayout() === 'grid' ? 'split' : 'grid'; } function save(): void { const snapshot = handle.value?.toJSON(); if (snapshot) localStorage.setItem('sales-dashboard', JSON.stringify(snapshot)); } </script> <template> <main> <nav> <button @click="tab = 'overview'">Overview</button> <button @click="tab = 'revenue'">Revenue</button> <button @click="switchLayout">Switch layout</button> <button @click="save">Save board</button> </nav> <GrafloriaDashboard :views="views" v-model:active-view="tab" :layout="layout" @ready="handle = $event" style="display:block; height:560px" /> </main> </template>
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.
| Option | Type | Default | What it does |
|---|---|---|---|
columns | number | 12 | Sets the column count for every view unless a view overrides it. |
gap | number | 8 | Sets the gap between widgets and the board padding, in pixels. |
sizing | 'fit' | 'grow' | 'grow' on fluid boards; 'fit' on fixed boards | grow 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. |
rowHeight | number | 130 | Sets row height in grow mode, in pixels. |
float | boolean | false | With false, gravity packs widgets upward; with true, gaps can remain where widgets are dropped. |
width, height | number | 1180 × 660 | Set a fixed board size. An explicit width selects fixed mode. |
static | boolean | false | Turns 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.
viewsand the single-viewwidgetsshorthand are mutually exclusive. Useviewswhen people switch between boards.- Framework wrappers mount the board once; changing data or
optionsafterward does not rebuild it. Use live props foractiveView,layout, andsizing, and use the handle for live board operations. - React custom widgets use
widgetTypes, notoptions.renderWidget; Vue useswidget-<kind>slots and Angular usesgrafloriaWidgettemplates. 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?