Floating panels
Float a panel out of the docked layout into a draggable, resizable overlay. Floats are first-class, serializable nodes that stay in the same app.
A panel can lift out of the tiled layout into a floating panel: a draggable,
resizable overlay that sits above the docked rows but stays in the same app (the
"float" move from FlexLayout). A floating panel is not a separate browser window;
it renders in the same React tree, so context, events, and styling stay connected.
It is also a first-class node in the model (Dashfoo.floats), so it serializes,
validates, and self-heals exactly like a docked panel. The
anatomy figure shows where floats and their
minimized chips sit relative to the tiled grid.
The floatable prop
Pass floatable to add a float control to each tabset toolbar and render any
floating panels:
import type { Dashfoo, TabNode } from "@dashfoo/core";
import { model, row, tab, tabset } from "@dashfoo/core";
import { DashfooLayout } from "@dashfoo/react";
import type { ReactNode } from "react";
import { useMemo } from "react";
const dashboard = (): Dashfoo =>
model(
row([
tabset([tab("chart", "Chart"), tab("trades", "Trades")], { id: "left", weight: 2 }),
tabset([tab("book", "Order Book")], { id: "right", weight: 1 }),
]),
);
const renderPanel = (node: TabNode): ReactNode => <div>{node.name}</div>;
const Dashboard = (): ReactNode => {
const defaultModel = useMemo(() => dashboard(), []);
return <DashfooLayout defaultModel={defaultModel} factory={renderPanel} floatable />;
};
export { Dashboard };Clicking the [data-dashfoo="tabset-float"] control floats the whole tabset into an
overlay positioned over where it was docked. Drag the title bar to move it
anywhere on the page, drag the edge/corner handles to resize, minimize it to
a small chip (and click the chip to restore), and Dock back to return it to the
layout. You can also drag tabs into and out of a float: it is a full
participant in drag-dock.
Floats are model nodes
Floating dispatches an action; the result is a normalized model with a floats
entry. Everything you can do to the main tree works on a float's tree:
import { reducer } from "@dashfoo/core";
const floated = reducer(model, { type: "floatTabset", tabsetId: "left" });
floated.floats;
// [{ type: "float", id, layout: RowNode, geometry: { left, top, width, height } }]
// Dock it back into the active main tabset and drop the float.
reducer(floated, { type: "dockFloat", floatId: floated.floats[0].id });The reducer actions are floatTab, floatTabset, dockFloat, moveFloat (persists
a float's rect as you drag or resize it, one undo step per gesture),
setFloatMinimized, and renameFloat. geometry is in viewport CSS pixels (the
overlay is fixed), so a float can be dragged anywhere on the page. A float carries
its own name ("Panel", "Panel 1", … assigned when it is floated), shown as the
window title, never the active tab's name. normalize heals each float's own
layout and drops a float the moment its last panel leaves, the same way it removes an
empty tabset. Because a float owns a full RowNode, a floated panel is a real
container: it can hold one tab or several.
Saved float bounds are projected into the current viewport, so restoring a desktop layout on a narrow screen keeps title-bar controls reachable. The projection creates no history entry and never overwrites the saved rect; widening restores the saved geometry. A later move or resize commits the geometry visible at that time.
What's in scope
- Same React tree, full drag-dock. Floats render through the normal
Layout.Rowsin the same tree, sharing one drag manager with the docked layout, so app context (a theme provider, a query client), event handling, your stylesheet, and dragging tabs into and out of a float all stay connected. - Drag anywhere + resize. The title bar moves the float across the viewport;
eight handles resize it. Both update the DOM imperatively during the gesture and
commit one
moveFloaton release, so a drag is a single undo step. - Named, renamable title. Each float is titled with its own unique name
("Panel", "Panel 1", …); double-click the title to rename it (
renameFloat), which dedupes the new name against the other floats so two never collide. - Minimize. A title-bar control collapses the float to a small chip
(
setFloatMinimized); tapping the chip (draggable itself) restores it to its saved rect. - Dock back as a panel. The "Dock back" control returns the float as its own panel (all its tabs, grouped) rather than flattening them into another tabset.
- Bring to front. Clicking anywhere in a float (its body, a tab, or its chrome) raises it above the others.
- Honors
editable. Undereditable={false}a float is static: its content stays selectable and it still raises to the front, but move, resize, rename, minimize, and dock-back are all switched off: none ofmoveFloat,renameFloat,setFloatMinimized, ordockFloatcan be dispatched from a static layout.
The overlay is pointer-events: none, so empty space stays click-through to the
docked layout underneath; each float opts pointer events back in.
Hand-built layouts
If you compose the Layout.* primitives yourself, wrap the layout's content with
Layout.FloatLayer (passing floats + global), which renders each floated panel
as an overlay over the rows, and add Tabset.FloatButton to your tabset toolbar.
<Layout.Root
dispatch={store.dispatch}
floatable
model={store.model}
renderers={{ tab: renderPanel }}
>
<Layout.FloatLayer floats={store.model.floats} global={store.model.global}>
<Layout.DragLayer>
<Layout.Rows node={store.model.layout} />
</Layout.DragLayer>
</Layout.FloatLayer>
</Layout.Root>Tabset.FloatButton hides itself (and warns) if no Layout.FloatLayer is present,
so a floated panel can never end up with nowhere to render.
See also: The layout model · Persisting layouts.