Picture someone who opens the same dashboard every morning for one figure: yesterday's revenue. It sits in the third row, under two charts they have never once looked at, because the layout was decided by whoever built it and nobody has asked since.
Interactive Dynamic Dashboard Pro, on my projects page, starts from the opposite assumption: the layout belongs to the person looking at it. It has sortable, draggable cards that stay where they are put, revenue tracking and real-time analytics, built in plain JavaScript and Bootstrap. These are notes on the decisions any rearrangeable dashboard forces, rather than a tour of one codebase.
A card is one question
The definition that holds up is: one question a person would ask out loud. "How much did we take this week?" is a card. "Sales" is a page pretending to be one.
Each card owns its title, its data request, its allowed sizes and its empty state. It also needs a stable identifier unrelated to its title or position. Titles get reworded and positions are what users change, so neither can be the key a saved layout uses.
What to save, and where
Save as little as possible: which cards, in what order, at what size. Not the data, and nothing the server can supply fresh. A layout is a preference, not a snapshot.
Where depends on how people work. One person on one machine is well served by the browser's local storage. Someone using a laptop and a phone needs the layout stored against their account, or they will arrange it twice and trust neither. Either way, a saved layout has to survive cards being added and retired. One sensible shape, called after every drop or resize:
const LAYOUT_KEY = 'dashboard-layout';
const LAYOUT_VERSION = 1;
const cardsIn = grid => [...grid.querySelectorAll('[data-card-id]')];
function saveLayout(grid) {
const cards = cardsIn(grid).map(el => ({ id: el.dataset.cardId, size: el.dataset.size }));
try {
localStorage.setItem(LAYOUT_KEY, JSON.stringify({ v: LAYOUT_VERSION, cards }));
} catch (e) { /* storage blocked: default layout still works */ }
}
function restoreLayout(grid) {
let saved = null;
try { saved = JSON.parse(localStorage.getItem(LAYOUT_KEY)); } catch (e) {}
if (saved?.v === LAYOUT_VERSION) { // a missing or older layout is ignored
const byId = new Map(cardsIn(grid).map(el => [el.dataset.cardId, el]));
for (const { id, size } of saved.cards) {
const el = byId.get(id);
if (el === undefined) continue; // card retired since the save
if (size) el.dataset.size = size;
grid.appendChild(el); // appending moves it into saved order
byId.delete(id);
}
byId.forEach(el => grid.appendChild(el)); // newer cards go last, not lost
}
}
The version number lets a later release discard a layout it no longer understands. Moving storage to the server changes the storage calls, not the shape.
Charts that survive being moved
A chart drawn for a wide card and dropped into a narrow column is the commonest way a rearrangeable dashboard looks broken. Many chart libraries measure their container once, so the chart keeps its old size: clipped, squashed or blurred.
Watch the card, not the window. A ResizeObserver on each card catches every size change, whatever caused it, and the chart can be redrawn once resizing settles rather than on every frame. Then decide what each size shows: a wide card carries axes and a legend, the same card at its smallest drops the legend and prints the latest value large. Skip the entrance animation on redraw, too: replayed whenever a neighbour moves, it is tiring to watch.
Drag and drop without a mouse
Dragging is a mouse and touch gesture. Without an alternative, the dashboard's central feature does not exist for a keyboard user.
The pattern that works: give every card a handle that is a real button. Space or Enter picks the card up, the arrow keys move it, Space or Enter drops it, and Escape cancels. Announce each step through a live region ("Revenue, position 2 of 8") so screen reader users hear where the card is. "Move up" and "Move down" in each card's menu make a good second route, and on a phone often the better one, since long-press dragging on a scrolling page is fiddly for everybody.
Before the data arrives
Live data means every card spends part of its life waiting. A loading placeholder should be the size of the finished card, so nothing jumps when the numbers land.
Empty and failed are different states. "No orders yet today" is information; a bare zero is ambiguous and a blank card looks broken. If one card's source fails, that card says so and the rest carry on. And every card should show when it last updated, because a number that quietly stopped refreshing an hour ago is worse than no number.
Opened every morning, or never
Whether a dashboard becomes a habit has little to do with how many charts it holds. It gets opened every morning when the first thing on screen answers the first question of the day, when every figure comes with something to compare it against, when it loads before the tea has gone cold, and when it is honest about how fresh its numbers are. It gets ignored when someone else decided what matters and the person using it cannot change that.
That is the real case for cards people can move. Dragging is only the mechanism. What changes is whose dashboard it is.