demo D306
A dashboard the reader can rearrange in ESM
Four views of one dataset in windows: blow one up to fill the dashboard, fold the rest away, unlock them to move, resize and close by drag or keyboard, and save the arrangement as JSON
createLayout · setInteractive · getLayout
Four different views of one set of field-service jobs sit in windows: a tile rail, the job list, where the money is by region, and the dispatch board. It opens finished and locked, any window can be blown up to fill the dashboard or folded out of the way, one button hands the arrangement over to the person using it, and whatever they end up with comes back out as plain JSON you can save against their name.
This is the ES module version. The grid is imported straight from a CDN as a module, the same configuration as the vanilla tab without a global script tag, so it drops into any bundler or a bare module script.
The configuration
<link rel="stylesheet" href="https://cdn.latticegrid.dev/lattice-grid@1.103.2/lattice-grid.min.css">
<button id="edit-toggle">Edit layout</button>
<button id="save-layout">Save layout</button>
<div id="dash" style="height: 780px; margin-top: 10px"></div>
<script type="module">
import { createGrid } from 'https://cdn.latticegrid.dev/lattice-grid@1.103.2/lattice-grid.esm.min.js';
import { createLayout } from '@toclocoinc/lattice-grid/modules/layout';
import { createChart } from '@toclocoinc/lattice-grid/modules/charts';
import { createKPI } from '@toclocoinc/lattice-grid/modules/kpi';
import { createKanban } from '@toclocoinc/lattice-grid/modules/kanban';
const editButton = document.getElementById('edit-toggle');
const saveButton = document.getElementById('save-layout');
// A view this sample mounted into a window, by window id, so a closed
// window's view is torn down rather than left running with nothing to hold
// it. In this trimmed sample only the grid, the tiles, the chart and the
// board are mounted; a fuller dashboard would register each here.
const views = {};
// Windows on a 12 by 10 cell grid. These three say nothing about moving,
// resizing or closing, so the dashboard arrives locked and looks finished. The
// rail says no to all three, which is how it stays pinned once the rest is
// unlocked. Every window can fill the dashboard or fold to its title bar,
// locked or not, except that the rail has no reason to fill it.
const layout = createLayout(document.getElementById('dash'), {
columns: 12, rows: 10, gap: 10,
maximisable: true, minimisable: true,
windows: [
{ id: 'health', title: 'Today at a glance', xPos: 1, yPos: 1, xSize: 12, ySize: 2,
movable: false, resizable: false, closable: false, maximisable: false },
{ id: 'jobs', title: 'Jobs', xPos: 1, yPos: 3, xSize: 7, ySize: 4 },
{ id: 'mix', title: 'Value by region', xPos: 8, yPos: 3, xSize: 5, ySize: 4 },
{ id: 'board', title: 'Dispatch board', xPos: 1, yPos: 7, xSize: 12, ySize: 4 },
],
});
// The layout makes each window's container and never reads what you put in
// one, so any view goes in any window. Size grid columns in pixels or with
// flex here, never as a percentage: a window that can be resized changes width.
const grid = createGrid(layout.payload('jobs'), { rowKey: 'id', rows: jobs, columns });
// A tile filter can test any grid column named in fields, so "P1 jobs" counts
// the P1 rows: { label: 'P1 jobs', aggregation: 'count', filter: (r) => r.priority === 'P1' }
createKPI(layout.payload('health'), { grid, fields: ['status', 'priority'], tiles });
createChart({ grid, container: layout.payload('mix'), type: 'bar', x: 'region', y: 'value' });
createKanban(layout.payload('board'), { rows: jobs, rowKey: 'id', columnProperty: 'status', columns: lanes });
// One button, both ways. Nothing is destroyed and nothing rebuilt, so every
// view survives the toggle with its scroll position, selection and data.
let editing = false;
editButton.addEventListener('click', () => {
editing = !editing;
layout.setInteractive(editing);
editButton.textContent = editing ? 'Done' : 'Edit layout';
});
// The arrangement is plain JSON. Save it wherever you keep a user's settings.
// { columns, rows, windows: [{ id, xPos, yPos, xSize, ySize }] }
saveButton.addEventListener('click', () =>
localStorage.setItem('dash', JSON.stringify(layout.getLayout())));
const stored = localStorage.getItem('dash');
if (stored) layout.setLayout(JSON.parse(stored));
// A closed window hands its container back and nothing else, so tear down
// whatever you mounted inside it.
layout.on('window:closed', ({ id }) => views[id]?.destroy());
</script>
A dashboard that opens finished, and can still be made theirs
The first thing anyone sees here is a screen, not a construction kit. Today’s jobs at a glance, the job list, where the value sits by region, and the dispatch board, all reading the same jobs. Nothing wobbles, no window offers a handle to drag it by, and a stray drag moves nothing. That is how a dashboard should arrive.
Press Edit layout and the same screen becomes editable where it stands. Handles appear on the windows that are allowed to move. Drag one by its title bar and the others get out of the way and close the gap behind it. Pull a bottom right corner and the window resizes, and the view inside it fits itself to the room it now has: the table shares its columns out again, the chart redraws into its new frame. Close one you do not want and the rest pull up into the space. Press Done and it is a finished screen again.
Nothing is torn down and rebuilt on the way through. The table keeps its place in the rows, the chart keeps its picture, and the board keeps its cards. Unlocking a dashboard costs the person using it nothing.
Every title bar is a solid navy band, so where one window stops and the next begins is never in doubt, and the bar you drag a window by is obvious the moment the dashboard unlocks. The colours are theme settings, so a dashboard in your own app can wear your own brand instead.
Blow one window up, fold the others away
A dashboard shares the screen out between its windows, which is right until someone needs to work in one of them. The two buttons at the right of each title bar are for that moment.
The square one fills the whole dashboard with that window. The job list shows more of its rows and columns, the chart redraws across the full width, and everything else steps aside until it is wanted. Press Escape, even from inside the table, or press the same button again, and every window is back where it was.
The line folds a window down to its title bar. Fold the job list and the chart, and the dispatch board moves up into the room they leave. Open them again in any order you like and each goes back exactly where it was.
Both work while the dashboard is locked, because neither one rearranges anything, so a finished screen can still be read one window at a time. Neither ends up in the saved arrangement either. Watch the JSON under the dashboard while you fill or fold a window: it does not change, so what you save is the dashboard as it is laid out, never as it happened to be showing.
The tile rail at the top can be folded but not blown up. Four tiles filling the screen would tell nobody anything more, so it says no to that, the same way it says no to moving.
Decide once what the whole dashboard allows
Watch the tile rail at the top when you press Edit layout. It does nothing. It stays exactly where it is while everything below it loosens up.
That is the useful half of the design. The three windows underneath say nothing at all about what they allow, and one call decides for all three at once, so “let people rearrange this” is one setting rather than an audit of every window you ever added. The rail says no, and no is final: unlocking never overrides a window you said should stay put. A capability can always be taken away, and it can never be granted where you said no, which is what makes a pinned masthead safe to rely on.
How do you save a dashboard layout a user has rearranged?
Read the arrangement back out and store it. It is plain JSON, and it is printed live under the dashboard on this page: drag a window and watch it change. All it carries is the size of the cell grid and where each window sits on it, so there is nothing to serialise, nothing internal to keep in step, and nothing that stops it going into a column in your own database next to a user id. Hand the same JSON back when they next sign in and the screen they built is the screen they get.
Press Save arrangement, move some windows about, then press Put it back. Even a window that was closed comes back. That round trip is the whole persistence story, and there is no part of it you have not just watched work.
It is fully operable from the keyboard
Everything above works from the keyboard, and not as a token gesture. Tab to a window’s handle, press Space to pick the window up, move it with the arrow keys, and press Enter to drop it. Escape leaves it where it was.
Each step is spoken, so someone working with a screen reader hears the window picked up, hears the column and row it is passing through as it moves, and hears where it landed.
The fill and fold controls are ordinary buttons in the title bar: tab to one and press Enter or Space. Each is named for what it will do next, so “Maximise Jobs” becomes “Restore Jobs” once the job list fills the dashboard, and the change is announced as it happens.
Why not reach for a free layout library?
GridStack is MIT-licensed and free, and it is not the only one. They are good at moving boxes around a page, and nothing here is a claim to be better at that.
The difference is what is in the boxes. A dashboard is only worth building if the table, the chart and the board are still right after somebody drags a corner, and a general-purpose layout library has no way to know they exist. It moves the box, and what happens inside it is between you and whatever you put there. That is the part that eats the afternoon.
Here the windows and the views in them are one product, and which views follow their window is written down and tested rather than something you learn from a support ticket. The grid and the chart follow their window, as you can prove on this page by dragging a corner. The tile rail and the board need no telling, because they already fit whatever room they are given. It is also a dependency you do not add and a second sizing model you do not have to learn.
What does a dashboard cost to leave open all day?
Nothing you can measure. There is no timer, no frame loop and nothing polling in the background, and twelve windows holding twelve independent live grids, watched for eight seconds in a real browser, cannot be told apart from a page with no dashboard on it at all. Both sit under a tenth of one percent of a processor core, which is the noise floor rather than a difference.
That matters for the screens people never close. An operations dashboard is open from the first coffee to the last, and it should not be the reason a laptop fan comes on.
To download, it is a 77,513-byte bundle gzipped, and that is the figure to budget for. Only 13,213 bytes of it is the layout’s own code; the rest is a fixed floor that every Lattice module carries. It brings no dependencies of its own.
What it will not do
Two limits worth knowing before you build on it.
There are no responsive breakpoints. Windows go on the cells you give them and stay there. Deciding that four windows across a laptop should become four stacked on a phone is your call, and this page makes it: it picks one of three arrangements from the width it actually has and hands it over. Narrow your browser and watch the note under the buttons change as it does. A twelve-window dashboard on a phone is not a solved problem and nothing here pretends otherwise.
And inside a window that can be resized, size a grid’s columns in pixels or let them flex, rather than giving them a percentage. A percentage width is worked out once against the page and not worked out again, so it is wrong from the moment the window around it changes size. This page sizes every column in pixels and shares the spare room out again whenever a window moves.