Docking layout

SvDockLayout: an IDE-style docking workspace - drag a tab onto another pane and drop on an edge to split or the centre to stack as a tab, drag the splitters to resize, close panes. The whole layout is a serializable, bindable DockNode tree.

A live, editable Svelte 5 component example from the SvGrid gallery (Layout). Read the SvGrid UI component docs for the full API.

About this example

SvDockLayout in Svelte 5, an IDE-style docking workspace. Drag a tab onto another pane and drop it on an edge to split or on the centre to stack it as a tab, drag the splitters to resize and close panes with the x. The whole layout is a serializable DockNode tree bound through bind:layout, so a workspace can be saved and restored.

SvDockLayout - a basic docking workspace (IDE-style). Drag any tab onto another pane and drop it on an EDGE to split or the CENTRE to stack it as a tab; drag the splitters to resize; close panes with the x. The whole layout is a serializable DockNode tree bound to layout.

Imports, features and API used

Imports: @svgrid/grid

Frequently asked questions

What does the layout tree look like?

A DockNode is either a split with a direction, sizes and children, or a pane with tabs and an active id. Every drag rewrites the tree and bind:layout reflects it, so JSON.stringify gives a saveable workspace.

How do I decide between split and stack?

By where you drop. The edges of a pane show a highlight for a split on that side; the centre highlights for adding the tab to that pane.

What happens when the last tab of a pane closes?

The pane is removed and its sibling takes the space, so the tree never keeps empty panes.

Related documentation

Related articles

Source code (361-dock-layout.svelte)

<script lang="ts">
  /**
   * SvDockLayout - a basic docking workspace (IDE-style). Drag any tab onto
   * another pane and drop it on an EDGE to split or the CENTRE to stack it as a
   * tab; drag the splitters to resize; close panes with the x. The whole layout
   * is a serializable `DockNode` tree bound to `layout`.
   */
  import { SvDockLayout, dockGroup, dockTabs, dockPane, type DockNode } from '@svgrid/grid'

  let layout = $state<DockNode>(
    dockGroup('row', [
      dockTabs([dockPane('explorer', 'Explorer')]),
      dockGroup('column', [
        dockTabs([dockPane('editor', 'index.ts'), dockPane('readme', 'README.md')]),
        dockTabs([dockPane('terminal', 'Terminal'), dockPane('problems', 'Problems')]),
      ], [0.68, 0.32]),
    ], [0.24, 0.76]),
  )

  const files = ['src/index.ts', 'src/app.svelte', 'src/lib/dock.ts', 'README.md', 'package.json']
</script>

<div class="wrap">
  <header>
    <h2>Docking layout</h2>
    <p>Drag a tab onto another pane - drop on an <b>edge</b> to split, the <b>centre</b> to stack as a tab. Drag the dividers to resize, and close panes with the x.</p>
  </header>

  <div class="stage">
    <SvDockLayout bind:layout>
      {#snippet pane(p)}
        {#if p.id === 'explorer'}
          <ul class="explorer">
            {#each files as f (f)}<li>{f}</li>{/each}
          </ul>
        {:else if p.id === 'editor'}
          <pre class="code">export function dock() {'{'}
  return 'drag me around'
{'}'}</pre>
        {:else if p.id === 'readme'}
          <div class="doc"><h3>README</h3><p>A basic docking layout built on a serializable tree.</p></div>
        {:else if p.id === 'terminal'}
          <pre class="term">$ npm run dev
  ready in 312 ms</pre>
        {:else if p.id === 'problems'}
          <div class="doc"><p>No problems detected.</p></div>
        {/if}
      {/snippet}
    </SvDockLayout>
  </div>
</div>

<style>
  .wrap { padding: 20px; display: flex; flex-direction: column; gap: 14px; height: 100%; box-sizing: border-box; }
  header h2 { margin: 0 0 4px; font-size: 20px; font-weight: 700; }
  header p { margin: 0; color: var(--sg-muted, #64748b); font-size: 13.5px; line-height: 1.5; }
  .stage { flex: 1; min-height: 420px; }
  .explorer { list-style: none; margin: 0; padding: 8px; font-size: 13px; display: flex; flex-direction: column; gap: 2px; }
  .explorer li { padding: 5px 8px; border-radius: 6px; cursor: default; }
  .explorer li:hover { background: var(--sg-row-hover-bg, #f1f5f9); }
  .code, .term { margin: 0; padding: 12px; font-size: 12.5px; line-height: 1.6; font-family: ui-monospace, monospace; }
  .term { color: var(--sg-muted, #64748b); }
  .doc { padding: 12px; font-size: 13px; }
  .doc h3 { margin: 0 0 6px; font-size: 14px; }
</style>

View this example on GitHub

More Layout examples

  • Account & security settings console - A real SaaS settings surface composed from the UI kit: SvMenubar app bar, promise + Undo-action toasts on save, SvPopconfirm on destructive rows, SvHoverCard teammate previews, and frame input adornments (leading icons, prefix affixes, masked API key). Profile / Team / Security tabs over SvCard + SvStat.
  • Invoice builder - An adornment-heavy money form: currency prefixes, % suffixes, masked tax IDs (frame adornments), line items with SvPopconfirm delete + Undo action toasts, live SvStat totals, an SvHoverCard tax hint, and a promise toast on Send. Pure UI-kit composition.
  • Operations KPI dashboard - A KPI console: SvStat + SvSparkline tiles, an SvGauge SLA dial, SvHoverCard drill-down previews per service, an SvMenubar toolbar (View / Range / Actions) and a promise toast on Refresh. Pure UI-kit composition, no grid dependency.
  • Form: rich field types - SvForm reaching the whole input suite from one schema - phone, country, mask, combobox, radio, slider, tags, datetime and file - plus per-field help text, a readonly field, column span, and a promise toast on submit.
  • Form: cascading fields - SvForm dependent selects - a child list derives from the parent value via a function `options`, and `dependsOn` clears the child when the parent changes so a stale selection never lingers. Country -> State -> City.