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. 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.
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.
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, a kebab overflow menu, and the close button (its hover tints --danger).
section
A stack-mode panel: a disclosure header (a rotating chevron and the title, the panel's controls beside it) over a body shown when expanded.
highlight
The drag-preview films over the live zones: 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
--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>