Dock Zone
A drag-and-drop dockable-panel workspace: tabbed zones that rearrange by dragging.
Live demo
Dock Zone is a movable-panel workspace, the editor-style chrome an app builds its layout from. Its direct children are the panels: any element with a data-panel-id and a data-title (or title) for its tab.
The zone reads them, arranges them into a layout of tabbed zones, and renders the tab strips and splits around them. Dragging a tab re-docks its panel onto another zone, joining it as a tab when dropped over the center or splitting the zone when dropped against an edge. Dragging a tab out past every zone tears it into a floating window that moves, resizes, and docks back on the same layout. A float's titlebar moves it: dragging anywhere across the open workspace just repositions the window, and a re-dock is only offered — films and all — once the pointer comes within the --dock-band of a zone's boundary, the seam a split would land against. Every rearrangement dispatches a layout-change event carrying the serializable layout, and setting the layout property restores a saved one, so a workspace persists across reloads. A panel carries its own header chrome, declared on the panel child: data-closable for a built-in close (a cancelable panel-close, then removal and a fresh layout-change), data-actions for direct header buttons, and data-menu for a kebab overflow <xtyle-menu>. A header button or a menu row both fire panel-action, and a data-badge puts trailing status text (a count) on the panel's own tab. A leaf renders in one of two modes: tabs (the default, one active panel behind a tab strip) or stack (every panel a collapsible section, the tool-rail shape); set mode for the whole workspace or per leaf in the tree. The layout physics are xtyle's own headless engine (resolveDrop for the drop geometry, dockPanel for the tree), the same primitives a consumer can drive directly from @xtyle/core/elements. The Svelte binding surfaces layout as a prop and reports rearrangement through onLayoutChange, close through onPanelClose, and header controls through onPanelAction; the Astro binding renders the panels and upgrades the workspace on the client. All of the chrome the workspace invents — the tab strips, the section headers and their chevrons, the header buttons and the kebab, the float windows and their resize grips, the drag films — is drawn by the component.dock-zone fragment, so an app reskins or restructures the panel chrome the same way a third-party mod would, while the element keeps the dock math, the pointer gestures, panel custody, and the persisted layout.
When to use
How this component composes with the rest of the set.
Props
11 props, straight from the manifest.
| Prop | Type | Default | Bindings | Description |
|---|---|---|---|---|
Anatomy
The named parts that make up the component, with their selectors.
root
The fragment's workspace root: the flex container the zones, the drag films, and the float layer all render inside. Everything below is drawn by the component.dock-zone fill, so a mod reshapes any of it.
zone
A leaf of the layout: a tab strip over the active panel's body.
tab
A draggable tab; the active one carries an accent underline. A data-badge renders trailing status text (.xtyle-dock-zone__badge, muted) after the title.
actions
The active panel's header controls, pinned to the end of the tab strip: direct action buttons, the kebab, and the close button (its hover tints --danger).
kebab
The overflow button a data-menu panel gets. A real button around a real ⋮ glyph (.xtyle-dock-zone__kebab-glyph), so a mod can swap the glyph for an icon or restructure the control; it opens the one cursor-anchored <xtyle-menu> the workspace shares.
section
A stack-mode panel: a disclosure header (a rotating chevron, .xtyle-dock-zone__chevron, and the title, the panel's controls beside it) over a body shown when expanded.
float
A torn-off panel's window: a titlebar (.xtyle-dock-zone__float-head) that drags the window around the workspace, with dock and close buttons, the panel's body, and a bottom-right resize grip (.xtyle-dock-zone__float-resize).
highlight
The drag-preview films, shown only while a drop is actually on offer: the drop target (--accent), the remnant a split would leave (--accent-2), and every other zone (--accent-3).
Tokens & coverage
What the component consumes, checked live against what the algorithm produces.
Live coverage check against the xtyle-default register
(derive(xtyleDefault, { anchors }) →
coverComponent(manifest, register)). Every token this component
consumes must be a key the algorithm produces.
--accent
--accent-2
--accent-3
--bg-1
--bg-2
--border-normal
--border-thick
--border-thin
--danger
--duration-fast
--ease-standard
--elevation-4
--fg-0
--fg-1
--fg-2
--font-sans
--line
--radius-md
--radius-sm
--ring
--space-0
--space-1
--space-2
--space-3
--space-4
--space-6
--space-8
--state-hover
--text-lg
--text-sm
--weight-medium
Slots
The panels: elements carrying data-panel-id and data-title. Their content shows in the active zone body. A panel can also carry its own header chrome: data-closable, data-actions, data-menu, and data-badge.
Accessibility
Code
Panels and persistence
Three panels in one zone; drag a tab to split or re-tab, and the layout persists to localStorage.
<xtyle-dock-zone style="height: 320px;">
<section data-panel-id="files" data-title="Files">The file tree.</section>
<section data-panel-id="outline" data-title="Outline">The document outline.</section>
<section data-panel-id="preview" data-title="Preview">The rendered preview.</section>
</xtyle-dock-zone>
<script type="module">
import "@xtyle/core/elements";
const zone = document.querySelector("xtyle-dock-zone");
zone.addEventListener("layout-change", (e) => {
localStorage.setItem("layout", JSON.stringify(e.detail.layout));
});
const saved = localStorage.getItem("layout");
if (saved) zone.layout = JSON.parse(saved);
</script>
<script lang="ts">
import { DockZone } from "@xtyle/svelte";
import type { DockLayout } from "@xtyle/core/elements";
// Restore a persisted layout in; read the current one back out via onLayoutChange.
let layout: DockLayout | null = $state(JSON.parse(localStorage.getItem("layout") ?? "null"));
</script>
<DockZone
{layout}
style="height: 320px;"
onLayoutChange={(e) => {
layout = e.detail.layout;
localStorage.setItem("layout", JSON.stringify(layout));
}}
>
<section data-panel-id="files" data-title="Files">The file tree.</section>
<section data-panel-id="outline" data-title="Outline">The document outline.</section>
<section data-panel-id="preview" data-title="Preview">The rendered preview.</section>
</DockZone>
---
import { DockZone } from "@xtyle/astro";
---
<DockZone style="height: 320px;">
<section data-panel-id="files" data-title="Files">The file tree.</section>
<section data-panel-id="outline" data-title="Outline">The document outline.</section>
<section data-panel-id="preview" data-title="Preview">The rendered preview.</section>
</DockZone>
<!-- The Astro wrapper renders the initial panels; wire persistence in a client
script that reads the element's `layout` and listens for `layout-change`. -->
Panel controls
A panel with a close button, a direct action button, and a kebab overflow menu, all declared on the panel child, all reporting through panel-action / panel-close.
<xtyle-dock-zone style="height: 320px;">
<section
data-panel-id="editor"
data-title="Editor"
data-badge="3"
data-closable
data-actions='[{ "id": "split", "label": "Split editor", "icon": "◫" }]'
data-menu='[{ "heading": "Panel" }, { "label": "Reveal in tree", "value": "reveal" }, { "separator": true }, { "label": "Close", "value": "close", "intent": "danger" }]'
>The document.</section>
<section data-panel-id="preview" data-title="Preview" data-closable>The rendered preview.</section>
</xtyle-dock-zone>
<script type="module">
import "@xtyle/core/elements";
const zone = document.querySelector("xtyle-dock-zone");
// A header button or a menu row: both arrive as panel-action.
zone.addEventListener("panel-action", (e) => {
if (e.detail.actionId === "close") zone.closePanel(e.detail.panelId); // same path as the ✕ button
else runCommand(e.detail.panelId, e.detail.actionId);
});
// The built-in close is cancelable; the zone removes the panel and re-reports the layout unless you veto it.
zone.addEventListener("panel-close", (e) => {
if (e.detail.panelId === "editor" && hasUnsavedChanges()) e.preventDefault();
});
</script>
Stacked rail
mode="stack" renders every panel as a collapsible section instead of a tab strip, the shape a tool or inspector rail wants. Per-leaf mode also lives in the layout tree, so a stacked rail and a tabbed editor coexist in one workspace.
<!-- A tool rail: every panel open at once, each a collapsible section. -->
<xtyle-dock-zone mode="stack" style="height: 320px; max-width: 280px;">
<section data-panel-id="layers" data-title="Layers">The layer list.</section>
<section data-panel-id="props" data-title="Properties">The property inspector.</section>
<section data-panel-id="history" data-title="History">The undo stack.</section>
</xtyle-dock-zone>
<!-- For a mixed workspace (a stacked rail beside a tabbed editor), author the leaf's mode in the tree: -->
<script type="module">
import "@xtyle/core/elements";
document.querySelector("xtyle-dock-zone").layout = {
kind: "split", direction: "row", sizes: [1, 3],
children: [
{ kind: "leaf", id: "rail", panels: ["layers", "props", "history"], active: 0, mode: "stack", collapsed: ["history"] },
{ kind: "leaf", id: "main", panels: ["editor", "preview"], active: 0 },
],
};
</script>