<bmx-dock-layout>
A docking workspace, as in an IDE or a trading desk: the page's panels are arranged in resizable rows and columns of tabbed groups. Drag a tab to another group, to a side of one (a new group is made there), to an edge of the whole workspace, or out into a floating window; fold a group to an edge where it slides out when wanted; maximise one for a while; close a panel and reopen it where it was. Everything can be done from the keyboard too, including the moves - Alt+Shift+M opens a dialog that chooses a place without a mouse.
6 properties · 3 events · 12 methods · 5 parts
Example
HP-1042 - Fenwick Joinery
Oak shelving, 12 units - due Thursday
- Depot
- Leeds North
- Carrier
- Northline, van 7
- Window
- 08:00 - 12:00
- Value
- £4,820.00
⚠️ HP-1039: the delivery window overlaps the depot's closing time.
⚠️ HP-1047: no carrier booked.
Drag a tab into another group, onto a side of one, to an edge, or hold Shift to float it.
Alt+Shift+M moves the panel from the keyboard; Alt+Shift+arrows send it to the next group.
F6 goes to the next group; Shift+F10 opens a tab's options.
Drag the tabs about, fold the Shortcuts group to an edge from its options, or type in Notes - it keeps what you typed wherever it goes.
Show markup
<bmx-dock-layout id="ex-dl" style="--bmx-dock-layout-height: 34rem">
<nav data-panel="orders" data-title="Orders" data-icon="📦" data-closable="false" style="padding: 8px; overflow: auto">
<ul id="ex-dl-orders" style="list-style: none; margin: 0; padding: 0; display: grid; gap: 4px"></ul>
</nav>
<section data-panel="details" data-title="Order HP-1042" data-closable="false" style="padding: 16px; overflow: auto">
<h3 id="ex-dl-heading" style="margin: 0 0 4px">HP-1042 - Fenwick Joinery</h3>
<p id="ex-dl-summary" style="margin: 0 0 12px; color: var(--bmx-text-muted)">Oak shelving, 12 units - due Thursday</p>
<dl style="display: grid; grid-template-columns: max-content 1fr; gap: 6px 16px; margin: 0">
<dt style="color: var(--bmx-text-muted)">Depot</dt><dd style="margin: 0">Leeds North</dd>
<dt style="color: var(--bmx-text-muted)">Carrier</dt><dd style="margin: 0">Northline, van 7</dd>
<dt style="color: var(--bmx-text-muted)">Window</dt><dd style="margin: 0">08:00 - 12:00</dd>
<dt style="color: var(--bmx-text-muted)">Value</dt><dd style="margin: 0">£4,820.00</dd>
</dl>
</section>
<div data-panel="notes" data-title="Notes" style="display: flex; padding: 8px">
<label for="ex-dl-notes" style="position: absolute; width: 1px; height: 1px; overflow: hidden; clip-path: inset(50%)">Notes on this order</label>
<textarea id="ex-dl-notes" style="flex: 1; resize: none; font: inherit; padding: 8px; border: 1px solid var(--bmx-border); border-radius: 6px; background: var(--bmx-surface); color: var(--bmx-text)">Call the site manager on arrival - loading bay 2.</textarea>
</div>
<div data-panel="activity" data-title="Activity" style="padding: 8px 12px; overflow: auto">
<ol id="ex-dl-activity" style="margin: 0; padding-inline-start: 18px; display: grid; gap: 4px; color: var(--bmx-text-muted)"></ol>
</div>
<div data-panel="problems" data-title="Problems" data-badge="2" style="padding: 8px 12px; overflow: auto">
<p style="margin: 0 0 6px">⚠️ HP-1039: the delivery window overlaps the depot's closing time.</p>
<p style="margin: 0">⚠️ HP-1047: no carrier booked.</p>
</div>
<div data-panel="help" data-title="Shortcuts" data-icon="⌨️" style="padding: 8px 12px; overflow: auto; font-size: 0.8125rem">
<p style="margin: 0 0 6px"><strong>Drag a tab</strong> into another group, onto a side of one, to an edge, or hold Shift to float it.</p>
<p style="margin: 0 0 6px"><strong>Alt+Shift+M</strong> moves the panel from the keyboard; <strong>Alt+Shift+arrows</strong> send it to the next group.</p>
<p style="margin: 0"><strong>F6</strong> goes to the next group; <strong>Shift+F10</strong> opens a tab's options.</p>
</div>
</bmx-dock-layout>
<p class="note" id="ex-dl-out" role="status" style="margin: 12px 0 0">Drag the tabs about, fold the Shortcuts group to an edge from its options, or type in Notes - it keeps what you typed wherever it goes.</p>
<script type="module">
await customElements.whenDefined('bmx-dock-layout');
const ws = document.getElementById('ex-dl');
const out = document.getElementById('ex-dl-out');
const orders = [
['HP-1042', 'Fenwick Joinery', 'Oak shelving, 12 units - due Thursday'],
['HP-1039', 'Castle Interiors', 'Pine desks, 4 units - due today'],
['HP-1047', 'Marsh & Co', 'Fittings, 30 boxes - due Friday'],
['HP-1050', 'Old Mill Studio', 'Oak shelving, 2 units - due Monday'],
];
const list = document.getElementById('ex-dl-orders');
for (const [id, customer] of orders) {
const li = document.createElement('li');
const button = document.createElement('button');
button.type = 'button';
button.textContent = `${id} · ${customer}`;
button.style.cssText = 'width: 100%; text-align: start; font: inherit; padding: 6px 8px; border: 0; border-radius: 6px; background: transparent; color: var(--bmx-text); cursor: pointer';
button.addEventListener('click', () => {
const [, name, summary] = orders.find(o => o[0] === id);
document.getElementById('ex-dl-heading').textContent = `${id} - ${name}`;
document.getElementById('ex-dl-summary').textContent = summary;
ws.querySelector('[data-panel="details"]').dataset.title = `Order ${id}`;
ws.openPanel('details');
});
li.append(button);
list.append(li);
}
document.getElementById('ex-dl-activity').innerHTML = '<li>08:12 HP-1042 picked</li><li>08:40 HP-1039 loaded on van 3</li><li>09:05 HP-1047 awaiting a carrier</li>';
// The layout in the shorthand: a row of three - the orders list, a column (details over notes) and the activity group.
ws.layout = {
row: [
{ stack: ['orders'], size: 22 },
{ column: [{ stack: ['details'], size: 62 }, { stack: ['notes', 'problems'], size: 38 }], size: 50 },
{ stack: ['activity', 'help'], size: 28 },
],
};
// Typing in the notes marks the tab with an unsaved-changes dot, and the page refuses to close it until it is saved.
const notes = ws.querySelector('[data-panel="notes"]');
document.getElementById('ex-dl-notes').addEventListener('input', () => notes.setAttribute('data-dirty', ''));
ws.addEventListener('bmxClose', e => {
if (e.detail.panel === 'notes' && notes.hasAttribute('data-dirty')) {
e.preventDefault();
out.textContent = 'bmxClose was cancelled: Notes has unsaved changes, so it stays open. (Your page would offer to save here.)';
}
});
ws.addEventListener('bmxChange', e => {
const layout = e.detail.layout;
out.textContent = `bmxChange: ${e.detail.reason}${e.detail.panels.length ? ` (${e.detail.panels.join(', ')})` : ''}. ${layout.floating.length} floating, ${layout.autoHide.length} folded to an edge, ${layout.closed.length} closed.`;
});
</script>
<bmx-dock-layout id="ws" persist-key="my-app">
<div data-panel="files" data-title="Files">...</div>
<div data-panel="editor" data-title="Editor" data-closable="false">...</div>
<div data-panel="terminal" data-title="Terminal">...</div>
</bmx-dock-layout>
<script>
ws.layout = { row: [{ stack: ['files'], size: 20 }, { column: ['editor', { stack: ['terminal'], size: 30 }], size: 80 }] };
ws.addEventListener('bmxChange', e => save(e.detail.layout));
</script>
Panels are the page's own elements, marked data-panel. They are never
moved in the document - each is shown through a slot in whichever group
holds it - so a panel keeps its state (scroll position, an editor's undo,
a playing video, an iframe that must not reload) wherever it is dragged.
The layout is plain JSON in layout and every bmxChange; nothing is
stored unless persist-key asks for the browser's local storage.
Accessibility: each group is a tab list with its tab panel; F6 moves
between groups and floating windows; splitters are focusable separators
with their value; every move, float, fold and close is announced. Narrower
than compact-below, the workspace becomes one tab list of every open
panel, so it works on a phone without changing the layout it keeps.
Properties
| Property | Attribute | Type | Default | Description |
|---|---|---|---|---|
compactBelow |
compact-below |
number |
600 |
Below this width (px) the workspace is shown as one tab list of every open panel. 0 never does. |
label |
label |
string |
— | The workspace's accessible name. Default: "Workspace". |
layout |
layout |
BmxDockArrangement | Record<string, unknown> | unknown[] | string |
'' |
The arrangement: the object bmxChange gives, JSON, or the shorthand ({ row: [...] }, { column: [...] }, { stack: ['a', 'b'], size: 30 }, a panel id for a group of one, an array for a row). Mutable: every change is written back as the full object. Panels the layout does not name join the first group. |
locked |
locked |
boolean |
false |
No dragging, closing, floating or folding; the groups can still be resized and their tabs chosen. |
persistKey |
persist-key |
string |
— | A name to keep the layout under in the browser's local storage, so it survives a reload. Nothing is stored without it. A stored layout wins over the first one the page sets after loading, which becomes the layout resetLayout goes back to. |
strings |
strings |
Partial<BmxDockLayoutStrings> | string |
— | Words to show instead of the English ones, as an object or JSON. |
Events
| Event | Detail | Description |
|---|---|---|
bmxActivate |
BmxDockActivateDetail |
Fired when a panel becomes the one showing in its group. |
bmxChange |
BmxDockChangeDetail |
Fired when the layout changes, with the whole new layout. |
bmxClose |
BmxDockCloseDetail |
Fired before the user closes a panel. Cancel it to keep the panel open. |
Methods
| Method | Signature | Description |
|---|---|---|
autoHide |
autoHide(panel: string, edge?: BmxDockSide) => Promise<void> |
Folds the group holding a panel to an edge (the nearest, if none is given). |
closePanel |
closePanel(panel: string) => Promise<void> |
Closes a panel (no bmxClose event: the page asked). |
floatPanel |
floatPanel(panel: string, rect?: Partial<BmxDockRect>) => Promise<void> |
Takes a panel out into a floating window (at rect, in pixels from the workspace's top-left corner, if given). |
getLayout |
getLayout() => Promise<BmxDockArrangement> |
The layout as it is now: plain JSON, to store and hand back through layout or setLayout. |
getPanels |
getPanels() => Promise<BmxDockPanelInfo[]> |
The panels the page has, with the titles and settings read from their attributes. |
maximise |
maximise(panel?: string) => Promise<void> |
Fills the workspace with the group holding a panel; no panel restores. |
movePanel |
movePanel(panel: string, target: BmxDockTarget) => Promise<void> |
Moves a panel into a group ({ stack, position: 'center', index }) or beside one, or to an edge of the workspace (no stack). |
openPanel |
openPanel(panel: string, target?: BmxDockTarget) => Promise<void> |
Shows a panel: brings it to the front of its group, slides out its folded group, or reopens it where it was. |
resetLayout |
resetLayout() => Promise<void> |
Goes back to the layout the page last set through layout (its default), whatever the user or storage changed since. |
restore |
restore() => Promise<void> |
Puts a maximised group back among the others. |
setFocus |
setFocus(panel?: string) => Promise<void> |
Puts keyboard focus on a panel's tab (default: the first group's showing panel). |
setLayout |
setLayout(layout: BmxDockArrangement | Record<string, unknown> | unknown[] | string) => Promise<void> |
Replaces the layout (the full form or the shorthand). resetLayout still goes back to the first one. |
Slots
| Slot | Description |
|---|---|
empty |
Shown when every panel is closed. |
CSS shadow parts
| Part | Description |
|---|---|
group |
A group of tabbed panels; also group-floating, group-flyout. |
splitter |
The bar between two groups. |
strip |
An edge strip of auto-hidden panels. |
tab |
A panel's tab; also tab-active. |
window |
A floating window. |
CSS custom properties
| Property | Description |
|---|---|
--bmx-dock-layout-accent |
The active tab's marker, the drop preview and the guides. |
--bmx-dock-layout-background |
Behind the groups (the gaps between them). |
--bmx-dock-layout-gap |
The width of a splitter. |
--bmx-dock-layout-group-background |
A group's body. |
--bmx-dock-layout-header-background |
A group's tab row. |
--bmx-dock-layout-height |
The workspace's height. |
--bmx-dock-layout-radius |
Corner radius of the groups and windows. |
--bmx-dock-layout-tab-height |
The height of a tab row. |