Skip to content
D
Documentation

Arrange dashboard containers

how-to
7 min readUpdated

Use a cell grid when widgets need column spans and gravity packing. Use a split layout when widgets need to cover the board and resize through percentage dividers. Both layouts use the same dashboard data; you can switch the mounted board without replacing its widgets.

The example starts with two KPI cards, an Operations section, and a tab container showing Filters. Its controls switch Grid/Split and Fit/Grow, mirror the board, toggle gravity, and change the section caption or active tab.

1. Declare the board

Install the packages for the binding you use in your framework project.

JavaScript:

bash
npm install @grafloria/element @grafloria/engine @grafloria/renderer

Angular:

bash
npm install @grafloria/angular @angular/common @angular/core @angular/forms @angular/platform-browser @grafloria/engine @grafloria/renderer rxjs @grafloria/element

Qwik:

bash
npm install @grafloria/qwik @builder.io/qwik @grafloria/element @grafloria/engine @grafloria/renderer

React:

bash
npm install @grafloria/react react react-dom @grafloria/element @grafloria/engine @grafloria/renderer

Vue:

bash
npm install @grafloria/vue vue @grafloria/element @grafloria/engine @grafloria/renderer

Put the following shared file beside your component or browser entry point. This example adds an inner grid and tab pages to the DashboardWidgetSpec entries in a DashboardViewSpec; see Build a dashboard for board and widget declarations.

The kit draws the kpi kind from your data, so this layout needs no chart renderer. DashboardOptions supplies the shared geometry. These options omit width: the board uses fluid sizing at zoom 1 and follows its container. sizing: 'fit' keeps its height bounded.

ts
import type {
  DashboardOptions,
  DashboardViewSpec,
  DashboardWidgetSpec,
} from '@grafloria/element';

const widgets: DashboardWidgetSpec[] = [
  {
    id: 'revenue', kind: 'kpi', span: 6, rows: 1,
    data: { label: 'Revenue', value: '$6.81M', delta: 12.4 },
  },
  {
    id: 'customers', kind: 'kpi', span: 6, rows: 1,
    data: { label: 'Customers', value: '1,284', delta: 8.1 },
  },
  {
    id: 'ops', title: 'Operations', span: 8, rows: 3,
    columns: 4, sizing: 'fit',
    caption: { subtitle: 'Today', description: 'Operational KPIs' },
    widgets: [
      {
        id: 'orders', kind: 'kpi', span: 2, rows: 1,
        data: { label: 'Orders', value: '312' },
      },
      {
        id: 'churn', kind: 'kpi', span: 2, rows: 1,
        data: { label: 'Churn', value: '1.9%' },
      },
    ],
  },
  {
    id: 'side', title: 'Side panel', span: 4, rows: 3,
    layout: 'tabs', active: 'filters', tabs: { stretch: true },
    widgets: [
      {
        id: 'filters', title: 'Filters', columns: 1, sizing: 'fit',
        widgets: [
          {
            id: 'region', kind: 'kpi', span: 1, rows: 1,
            data: { label: 'Region', value: 'EMEA' },
          },
        ],
      },
      {
        id: 'alerts', title: 'Alerts', columns: 1, sizing: 'fit',
        widgets: [
          {
            id: 'open-alerts', kind: 'kpi', span: 1, rows: 1,
            data: { label: 'Open alerts', value: '3' },
          },
        ],
      },
    ],
  },
];

export const views: DashboardViewSpec[] = [{ id: 'main', widgets }];
export const options: Partial<DashboardOptions> = {
  columns: 12,
  gap: 10,
  sizing: 'fit',
  responsive: { columnWidth: 80 },
  dragHandle: { grip: true, position: 'right', placement: 'inside' },
};

At narrower widths, responsive derives fewer columns, capped by columns. Widening again restores the cached wide layout. Omitted x and y place widgets in declaration order, wrapping at the column count.

2. Mount it and bind the switches

Use dashboard() and render() in JavaScript. render() mounts the spec and initializes its live DashboardHandle. Its return value is the renderer's DiagramInstance; keep that for teardown, and use the dashboard handle for board operations.

For framework components, use GrafloriaDashboardComponent in Angular, GrafloriaDashboard in Qwik, GrafloriaDashboard in React, or GrafloriaDashboard in Vue. Each binding supplies the handle through its ready event. Bind layout and sizing as props or inputs: changes call the live handle rather than remounting the board.

Choose one tab below. The framework examples are components to render in your application; the JavaScript entry point runs in the browser. All import ./board from step 1.

ts
import { dashboard, render } from '@grafloria/element';
import { options, views } from './board';

export function mountDashboard(parent: HTMLElement): () => void {
  const wrapper = document.createElement('section');
  const toolbar = document.createElement('div');
  const host = document.createElement('div');
  host.style.height = '480px';
  wrapper.append(toolbar, host);
  parent.append(wrapper);

  const spec = dashboard({ ...options, views });
  const instance = render(spec, host);
  const handle = spec.handle;
  const button = (label: string, action: () => void): void => {
    const control = document.createElement('button');
    control.textContent = label;
    control.addEventListener('click', action);
    toolbar.append(control);
  };
  button('Grid', () => handle.setLayout('grid'));
  button('Split', () => handle.setLayout('split'));
  button('Fit', () => handle.setSizing('fit'));
  button('Grow', () => handle.setSizing('grow'));
  button('RTL', () => handle.setRtl(!handle.getRtl()));
  button('Float', () => handle.setFloat(!handle.getFloat()));
  button('Caption chip', () => {
    handle.setCaption('ops', { text: 'Operations', position: 'tab', icon: '▤' });
  });
  button('Alerts tab', () => { handle.activateTab('side', 'alerts'); });
  button('Frame board', () => handle.fit());

  return () => {
    instance.dispose();
    wrapper.remove();
  };
}

mountDashboard(document.body);

The JavaScript sample starts with Revenue and Customers above Operations and the Filters page.

JavaScript board with the layout toolbar, Operations caption, and Filters and Alerts tabs.

The Angular component shows Orders and Churn inside Operations, beside Region in the active Filters page.

Angular board with two top-level KPI cards, the Operations section, and Filters selected.

The Qwik component shows the same initial grid with the toolbar above it.

The React component starts with the Operations subtitle Today and the Filters page showing EMEA.

The Vue component also starts in the grid, with Filters selected and Alerts available beside it.

Press Split to replace grid resize corners with dividers; drag a divider to change pane proportions. Press Grid to project the split tree back into cells. These controls change the view's layout, not the Operations section's inner grid or the side panel's tabs. To switch a section or page separately, pass its id as the second argument to setLayout(), such as setLayout('split', 'ops').

Press Grow in grid mode to keep row heights and let the board extend downward; Fit keeps the board height and squeezes rows. Split layout always fits, so it does not become a growing grid when you press Grow. Frame board calls fit() to reframe the current view's camera—it does not change the sizing mode.

The ready callback gives Qwik a live handle in the browser; noSerialize() keeps it out of resumable state. The framework components dispose their renderer on unmount. In JavaScript, keep the function returned by mountDashboard() and call it when your application removes the panel.

3. Tune the geometry and interaction

Fixed size or fluid size

For a fixed design surface, replace the shared options export with mode: 'fixed', width: 1180, height: 660 alongside your other options. The camera frames those authored dimensions. An explicit width also implies fixed mode unless you set mode yourself. A fluid board takes its dimensions from the container instead; see Theme a canvas for container sizing.

The following options govern the board, not the renderer's zoom-to-content operation:

OptionTypeDefaultWhat it does
mode'fluid' | 'fixed''fluid'; explicit width implies 'fixed'Uses container pixels at zoom 1, or an authored world size framed by the camera.
width, heightnumber1180, 660Defines the fixed board size in pixels.
layout'grid' | 'split''grid'Packs cells or fills the board through a splitter tree. Split always fits.
sizing'fit' | 'grow''grow' fluid; 'fit' fixedSqueezes rows within the height, or keeps row height and extends downward.
rowHeightnumber130Sets row height in growing grids, in pixels.
overflow'bounded' | 'scroll''bounded'Enforces fit capacity, or opts into an extending frame and camera panning.
squeezebooleantrueSqueezes bounded fit rows toward the row floor before refusing growth. false freezes the current row height for gestures.
columnsnumber12Sets the grid's column count; views and containers can override it.
gapnumber8Sets widget gaps and board padding in pixels.
floatbooleanfalseAllows gaps when true; otherwise gravity packs widgets upward.
rtlbooleanfalseMirrors pixels so cell x=0 appears on the right; cells stay unchanged.
responsiveDashboardOptions['responsive']Not setDerives live columns from width, using column-width rules or breakpoints.
dragHandleDashboardOptions['dragHandle']falseChooses the whole card, caption, selector, or painted grip as the drag zone.
staticbooleanfalseDisables pointer dragging, resizing, and handles; API edits still work.

A bounded fit grid refuses a drop, resize, or addWidget() that needs more rows than its capacity. addWidget() returns undefined when the widget does not fit. An already oversized board—loaded from a document or switched from Grow—still squeezes to the frame rather than scrolling. With overflow: 'scroll', you explicitly opt out of that bound.

Gravity, RTL, and columns

Use Float in grid mode to toggle whether gaps are legal. Turning float off repacks widgets upward. In the Vue sample, RTL calls setRtl(true) to mirror widget pixels without rewriting their cells; repeated clicks keep RTL enabled rather than reversing the mirror.

For explicit column controls, call setColumns(6, 'moveScale', 'main') on the ready handle. It returns void; read getColumns('main') for the live count. The optional reflow argument uses GridColumnLayout:

Reflow modeResult
'moveScale' (default)Scales both horizontal position and span by the new/old column ratio.
'move'Scales position; keeps spans, clamped to fit.
'scale'Scales spans; keeps positions, clamped to fit.
'none'Keeps positions and spans; clamps only what no longer fits.

One column forces a single stack in every mode. A manual setColumns() call pins the grid's column count instead of continuing to follow the width observer. Keep the sample's responsive rule for automatic resizing, or use explicit column controls for a user-selected count; do not expect both to drive the same grid simultaneously.

Sections, captions, tabs, and grips

Operations has four inner columns regardless of its outer span. Its sizing: 'fit' refuses child growth beyond the pane; a container's default 'grow' instead grows its slab in the parent when a child needs another row. maxRows defines the inner grid's designed row count, while limits.maxRows limits a widget's own resize. Containers can nest; nesting beyond two levels is not exercised by the library's gates.

SectionCaption accepts true for the section title, a string for explicit text, false for no caption, or SectionCaptionOptions. The initial subtitle reserves a second caption line. Caption chip changes it to a title-sized chip at the section's leading corner; both position: 'inside' and 'tab' reserve space inside the section. setCaption() returns false for a non-container; a successful change repaints and records one undo step.

For caption behavior, show: 'design' removes the band and its reserve in static mode; show: 'hover' overlays content without reserving space. Caption action buttons call onCaptionAction instead of selecting the section.

TabsOptions controls the strip's alignment, height, and stretching. The sample stretches Filters and Alerts across the strip. Alerts tab calls activateTab('side', 'alerts'): it shows that page and persists the active id. The call returns false for an invalid container or page. Use setLayout('split', 'alerts') to change that page's inner layout, not the tab container itself. setLayout() accepts only grid and split; declare a tab container with layout: 'tabs' in its widget spec.

DragGripOptions places a painted grip at the left, center, or right of the card's top edge, inside the header or outside as a tab. The sample chooses the right side inside the header. Click a card to select it: only the selected card shows its grip. Call focusWidget('revenue') on the ready handle to select it and move keyboard focus there; the method returns false for an unknown id.

To make the entire caption the drag target, call setDragHandle(true); use false for the whole card, or a selector string for your own handle element. Static mode paints no grip. These handle switches apply across views.

See it running

Open the Fluid dashboard demo to try Fit/Grow, Grid/Split, captions, tabs, and drag grips. The Grid options demo explores gravity, RTL, responsive columns, and nested boards.

Was this page helpful?

Arrange dashboard containers — Grafloria