Dockable
Guides

Borders

Side bars with tabs that open panels beside or over the layout - split and overlay borders, auto-hide, resizing, edge docking.

A border is a strip of tabs on one side of the layout, like an IDE's side bars. Its selected tab opens a panel next to the strip; clicking the selected tab again closes it. Borders are part of the model, so they are saved, restored and changed through actions like everything else.

Intermediate exampleBordersSide bars with tabs, as in an IDE: an explorer on the left, a terminal below, an outline on the right. Click a border's tab to open its panel beside the layout, again to close it; resize it with its splitter; drag tabs into a border and out of it.Open the live example

1. Declare them in the model

Borders live in the JSON's borders, one per side at most:

const json: IJsonModel = {
    global: { borderSize: 220 },
    borders: [
        {
            type: "border",
            location: "left",
            selected: 0, // open on its first tab; -1 (the default) is closed
            children: [
                { type: "tab", name: "Explorer", component: "explorer" },
                { type: "tab", name: "Search", component: "search" },
            ],
        },
        {
            type: "border",
            location: "bottom",
            size: 160,
            children: [{ type: "tab", name: "Terminal", component: "terminal" }],
        },
    ],
    layout: { type: "row", children: [/* … */] },
};

A border's tabs are ordinary tabs: Dockable.Panels renders their panels like any other, and their content keeps its state when they move between a border and a tabset. The attributes are in the JSON model reference (size, minSize, maxSize, show, enableAutoHide, borderType, enableDrop, autoSelectTabWhenOpen/WhenClosed, and their border… globals).

2. Render the frame, the strips and the panels

Dockable.Borders replaces the root Dockable.Row inside Dockable.Root, and takes the row as its child. It lays the borders out around the main area, as FlexLayout does: the top and bottom strips span the full width, the left and right strips sit between them, and each panel area sits between its strip and the layout.

<Dockable.Root model={model}>
    <Dockable.Borders
        renderBar={(border) => (
            <Dockable.Border node={border} className="strip">
                <Dockable.TabList aria-label={`${border.getLocation().getName()} panels`}>
                    {(tab) => (
                        <Dockable.Tab node={tab} className="border-tab">
                            {tab.getName()}
                        </Dockable.Tab>
                    )}
                </Dockable.TabList>
            </Dockable.Border>
        )}
    >
        <Dockable.Row>{renderNode}</Dockable.Row>
    </Dockable.Borders>
    <Dockable.Panels>{(tab) => <Dockable.Panel node={tab}>{content(tab)}</Dockable.Panel>}</Dockable.Panels>
</Dockable.Root>
  • renderBar renders a border's strip: a Dockable.Border with a Dockable.TabList. The tab list runs vertically in a left or right border (aria-orientation, arrow keys).

  • renderContent (optional) renders the panel area: by default a Dockable.BorderContent, which holds an area sized by the border (the engine positions the selected tab's panel over it) and the border's splitter on the layout's side. Pass your own to style it, or to replace the splitter:

    renderContent={(border) => (
        <Dockable.BorderContent
            node={border}
            renderSplitter={(node) => <Dockable.Splitter node={node} className="splitter" />}
        />
    )}

Borders only render in the main layout, never in a popout window.

3. Style the strips

The primitives set only structural flex. Everything else is yours, through data-*:

onattributemeaning
Border, BorderContentdata-locationtop, bottom, left or right
Border, BorderContentdata-orientationvertical for a left or right border (the direction its tabs run)
Border, BorderContentdata-opena tab is selected: the panel is open
Border, BorderContentdata-overlay / data-dockedan overlay border / a split border
Borderdata-tab-directionon a left border: up or down (borderLeftTabDirection)
Borderdata-drop-targetthe current drag would drop into the border

Side labels are the classic question. Turn them with writing-mode (the tabs stay in order from the top), and turn a left border's half a turn more when it reads "up", FlexLayout's default:

[data-location][data-orientation="vertical"] [role="tab"] {
    writing-mode: vertical-rl;
}
[data-tab-direction="up"] [role="tab"] {
    transform: rotate(180deg);
}

With Tailwind, as the examples do: in-data-[orientation=vertical]:[writing-mode:vertical-rl] in-data-[tab-direction=up]:rotate-180 on the tab. Drops into the strip are hit-tested against the tab buttons' measured boxes, so any styling works.

Tab orientation is styling

Nothing in the package turns a side border's labels: Dockable.Border lays its tab list out as a column and exposes data-orientation and data-tab-direction. Vertical labels, upright labels in a column, or icon-only tabs as in an IDE's activity bar are all class names:

// an activity bar: an upright icon per tab, named for assistive technology
<Dockable.Tab node={tab} aria-label={tab.getName()} className="grid place-items-center p-2">
    <FilesIcon aria-hidden />
</Dockable.Tab>

An icon-only tab has no text: give it an aria-label (and a tooltip for sighted users). The border tab orientation example switches the same borders between vertical and upright labels, and the IDE workbench has an activity bar.

Intermediate exampleBorder tab orientationHow a side border's tabs read is styling, not the package: the same left and right borders with their labels turned vertical (the kit's default, with writing-mode) or upright in a column. The toggle only swaps class names; Dockable.Border sets nothing but structural flex.Open the live example

4. Open, close and resize

  • Click a tab to open its panel; click the selected tab again to close it. Both are Actions.selectTab (for a border's selected tab the model deselects), so onAction sees them.
  • Resize with the border's splitter: drag it, or focus it and use the arrow keys (10px a step, towards the layout). Its ARIA value is the size in px (aria-valuenow, aria-valuemin, aria-valuemax from size, minSize, maxSize). The action is Actions.adjustBorderSplit.
  • From code: Actions.selectTab(tabId) opens (or closes) a border's tab, Actions.adjustBorderSplit("border_left", 300) resizes it, and Actions.updateNodeAttributes("border_left", { show: false }) hides the whole border. Border ids are border_<location>.

5. Drag tabs in and out

Borders take part in drag and drop:

  • a tab dropped on a strip joins the border at that position;
  • a tab dropped on an open panel joins the border (appended);
  • a border's tab dragged into a tabset (or to a layout edge) leaves the border.

enableDrop: false on a border (or borderEnableDrop globally) refuses drops into it, and onAllowDrop sees border targets like any other. Dockable.Border carries data-drop-target while it is the target.

Overlay borders

Intermediate exampleOverlay bordersBorders whose panels slide over the layout instead of shrinking it, and close on a click elsewhere or Escape. Switch each border between split and overlay; an empty auto-hide border on the right appears while a tab is dragged near that edge.Open the live example

An overlay border (borderType: "overlay") opens over the layout instead of beside it, like a drawer:

{ type: "border", location: "left", borderType: "overlay", children: [/* … */] }

Switch one at runtime with Actions.setBorderType("border_left", "overlay") (or "split").

  • It closes on a press in the layout outside its panel (a tabset, a splitter), and on Escape (keyMap.closeOverlayBorder) with focus in the panel or on its tab button; focus goes back to the tab button. A press on a border strip does not close it. Both close paths are Actions.selectTab through onAction.
  • Dockable.BorderContent is position: absolute against the layout's edge, and lets presses through to the tab panel (pointer-events: none, except on its splitter). A left or right overlay stops above and below the open top and bottom overlays.
  • Its stacking is yours: give the content a z-index so its splitter paints above the tabsets (the examples use data-overlay:z-30), and a shadow if you like.

Auto-hide borders

With enableAutoHide (or borderEnableAutoHide), a border with no tabs is not rendered, so it takes no space. While a tab is dragged near that border's edge of the layout (within edgeDockMargin, away from the edge docking band in the middle of the edge), the border appears so the tab can be dropped into it; it hides again when the drag moves away or ends. Once it has a tab, it shows like any border.

Edge indicators

A drop in the band along an edge of the layout docks the tab to that whole edge, as a new tabset. Dockable.EdgeIndicator marks those bands during a drag:

{(["top", "bottom", "left", "right"] as const).map((edge) => (
    <Dockable.EdgeIndicator key={edge} edge={edge} className="edge-indicator">
        <ArrowIcon edge={edge} aria-hidden />
    </Dockable.EdgeIndicator>
))}

Each indicator is positioned over its band (edgeDockMargin deep, edgeDockLength long, centred on the edge), shown while a drag that can dock to the edges is over the layout, and carries data-drop-target when the drop would dock there. It is pointer-events: none, and its stacking is yours. See Dockable.EdgeIndicator.

The band is 10px deep by default. A thin tab strip at the top of the layout sits inside it, so a drop aimed at the strip's middle docks to the top edge instead (gap 11 in Limitations). Lower edgeDockMargin in the global attributes, or keep tab strips taller than twice the band.

Borders in the other examples

The IDE workbench keeps its explorer in a left border and the terminal and problems in a bottom border, and the drag and drop example shows the edge indicators.

On this page