Blog / Engineering

Sections that push, tabs that tear out

October 13, 2026 · dashboards · layout

dashboards layout

Our last dashboard post was about fluid boards, a split layout and a drag grip, in @grafloria/element 0.4.9. The kit is at 0.7 now, with a package of its own, @grafloria/dashboard. This is what changed in between that you would notice in an app: sections that push, captions the kit paints, tab containers you can tear apart, drops that mean what they show, and a few fixes you only meet when they bite.

Sections are solid, and a growing section pushes

A section is a tile that holds tiles: a "Revenue" panel with three charts in it. The first rule we changed is that a section is solid. A widget dragged across the board no longer shoves a section out of its way the way it shoves a small tile; it is refused, and the section stays put. A section moves only when you mean it to: when you drag the section itself, or when something next to it grows.

Growing is where the old kit gave up. Pull a chart inside a section taller than the section's rows and the section grows to hold it. But until 0.7, if another section sat below, the growth stopped at it and the pull was capped. Now the growing section pushes everything below it down, other sections and tab containers included, and the whole thing is one undo step. The same is true from code:

const board = dashboard({ /* … */ });
// Past its section's rows: the section grows by the rows it needs, and pushes what is below it.
await board.widget('k-rev').resize(1, 2);
// One ⌘Z puts the widget, its section and everything that moved back.

A section that should never change size says so: with sizing: 'fit' the section is the bound, and a child that needs a row it does not have is refused instead.

Captions the kit paints

A section can carry a caption band: a title, a subtitle, an icon, and buttons. You describe it as data, and the kit paints it, keeps it out of the way of its neighbours, and tells you when a button is pressed:

{ id: 'sales', widgets: [/* … */],
  caption: {
    text: 'Sales', subtitle: 'This quarter', icon: '📈',
    position: 'inside',            // or 'tab': a tab above the frame
    show: 'always',                // or 'design', 'hover'
    actions: [{ id: 'export', label: 'Export' }],
  } }
// options: onCaptionAction: (sectionId, actionId, viewId) => …   handle.setCaption(id, caption)

The actions are real buttons, a press on one never selects the section, and the band is a group labelled with its section's name for assistive technology. When the band is not enough, renderCaption lets you paint your own.

Tab containers

A section with layout: 'tabs' shows one page at a time behind a tab strip. The strip is a proper tablist: one tab stop, arrow keys to move, Home and End to jump, and the arrows mirrored on right-to-left pages. From code, activateTab, getActiveTab, moveToTab and moveTab drive it, and onTabChange reports it.

Tabs can also be taken apart by hand, the way editor tabs can. Drag a tab out of its strip and the page becomes a section of its own where you drop it; a chip with the tab's label follows the pointer and a dashed placeholder shows where it will land. Drop it on another group's strip and it joins that group; on a split board, drop it beside a pane and it becomes a pane. Undo puts it back and shows the page that was showing before the drag, not whichever page happened to be first.

For read-only viewers there is tabs: { hidden: true }: no strip at all, nothing reserved for it, pages switched from code. A designer leaves it off so the author can reach every page.

Drops that mean what they show

On a board with sections inside sections inside tabs, "where will this land?" used to have several answers depending on which code path asked. There is now one recursive answer: the same resolver decides what the placeholder shows and what the drop does, down to the deepest board the nesting option allows. A widget dropped on a section's caption band means "into this section". A drop that will be refused shows its cell in red under the pointer, so a refusal is visible before you let go. And a palette drop through addWidget(spec, viewId, { displaced }) lands exactly on the cell the preview showed, pushing the same tiles the preview pushed.

Fixes you only meet when they bite

⌘Z undid every board on the page. With two diagrams or dashboards on one page, a single ⌘Z undid a step in each of them, and one Delete deleted in each. Keys now act on the canvas you last pressed or focused. If your own Undo button calls getEngine().commandManager, nothing changes for it.

Disposing the diagram left the board behind. Disposing the render instance did not dispose the dashboard handle on it, so the old board's tools kept taking presses on the next board mounted in the same element. React's StrictMode, which mounts everything twice in development, made this easy to hit. Disposing the diagram now disposes its board too; calling both is still fine.

A saved board carried functions. toJSON() returns plain data now: no callbacks, nothing that cannot go through JSON.stringify and come back.

Split views forgot your sizing. A split view always lays out to fit its frame, but switching a view to split and back used to leave it in fit mode. The board now keeps the sizing you chose, the split view draws fit anyway, and switching back to grid gives you your setting again, saved in toJSON.

What we got wrong

The section that stopped growing was found by a review of our documentation, not by our checks, because every check grew a section with nothing below it. The scenario that now covers it puts a second section directly underneath and pulls a widget past the first one's rows, and it fails without the fix. While fixing it we found that resize() from code still refused where a hand pull grew: two paths to the same edit had drifted apart. Both now grow the section the same way, and each has its own scenario.

Get it

Install @grafloria/dashboard, or keep using @grafloria/element, which it re-exports. Everything above is in 0.7, MIT. The fluid dashboard has tabs, captions, splits and sizing in its toolbar, the nested containers demo has sections in sections, and the dashboard builder shows palette drops. For a board with stronger opinions, read A Kanban board is a dashboard with opinions.