Tab/split panel layout for Svelte 5. Drag-drop tabs between groups, split a
group horizontally or vertically, stack mode for grids of small panels, and
persist the entire layout tree to localStorage.
Host application provides the panel components; the library handles the layout state, tab rendering, drag-drop, and splitter resizing.
- Tab groups with reorderable, draggable tabs.
- Split groups (
row/column) with resizable splitters. - Stack mode — auto-grid of all tabs in a group, useful for dashboards.
- Drag a tab to the edge of any group → splits in that direction.
- Layout tree serialised + restored from
localStorageon load. - Svelte 5 native (uses
$state/$effectrunes). - ~950 LOC, no runtime dependencies beyond Svelte itself.
# As a git dependency
npm install github:Geptyro/panels-layout
# Or from a monorepo
"panels-layout": "file:../packages/panels-layout"Requires svelte ^5.0.0 as a peer dependency.
// panels.js
import HomePanel from '$lib/panels/HomePanel.svelte';
import SettingsPanel from '$lib/panels/SettingsPanel.svelte';
import LogsPanel from '$lib/panels/LogsPanel.svelte';
export const panelList = [
{ type: 'home', label: 'Home' },
{ type: 'settings', label: 'Settings' },
{ type: 'logs', label: 'Logs' },
];
export const components = {
home: HomePanel,
settings: SettingsPanel,
logs: LogsPanel,
};// app.js
import { configureLayout, nodeFactories } from 'panels-layout';
import { panelList, components } from './panels.js';
const { makeGroup, makeSplit } = nodeFactories;
configureLayout({
panelList,
components,
storageKey: 'my-app-layout-v1',
buildDefault: () => makeSplit('row', 0.3,
makeGroup(['home']),
makeGroup(['settings', 'logs']),
),
});<!-- App.svelte -->
<script>
import { LayoutNode, layout } from 'panels-layout';
</script>
<div class="app">
{#if $layout}
<LayoutNode node={$layout} />
{/if}
</div>
<style>
.app { width: 100vw; height: 100vh; }
</style>That's it. Drag a tab between groups, or to the edge of one to split,
or use setGroupMode(groupId, 'stack') for the grid view.
import {
// Components
LayoutNode, Splitter, TabGroup,
// Setup
configureLayout,
nodeFactories, // { makeTab, makeGroup, makeSplit }
// Stores (Svelte writable)
layout, // tree of split/tabs nodes
activeGroupId, // currently focused group
dragState, dropTarget, // drag-drop state (rarely read directly)
// Actions
setActiveGroup, setActiveTab, setGroupMode,
addTab, closeTab, moveTab,
setRatio, splitGroup, splitAtAncestor, splitAndMoveTab,
openOrFocus,
resetLayout,
// Helpers
getPanelList, getPanelComponent, getAllGroups,
} from 'panels-layout';configureLayout({...}) must be called once before any other API:
| Field | Type | Purpose |
|---|---|---|
panelList |
[{type, label}] |
Catalog of available panels |
components |
{type: SvelteComponent} |
Panel-type → component map |
storageKey |
string |
localStorage key for persistence |
buildDefault |
() => node |
Returns the initial tree if nothing's persisted |
The layout is a recursive tree of two node kinds:
// Tab group — leaf in the tree.
{
type: 'tabs',
id: 'g…',
mode: 'tabs' | 'stack',
tabs: [{ id, panelType, title, state? }, …],
active: 't…',
}
// Split — internal node, exactly two children.
{
type: 'split',
id: 's…',
dir: 'row' | 'column',
ratio: 0.5, // 0..1, child A's share
a: <node>,
b: <node>,
}buildDefault() returns one of these, optionally nested. Use nodeFactories
helpers (makeGroup, makeSplit, makeTab) instead of building object
literals — they generate the IDs and apply the right defaults.
- Single layout instance per app.
configureLayoutis global — internally a singleton. Multiple independent layouts in one app aren't supported (yet). - No SSR. localStorage access happens at module init; mount in a client component / browser-only entry.
- No keyboard shortcuts. Mouse-only for now.
# In a monorepo, this package has no build step — Svelte components are
# imported directly from src/. Hosting apps consume via the `svelte` field
# in package.json.MIT — see LICENSE.