Skip to content

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

2 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

SuperSplit

A dependency-free tiling split-layout engine for the web. Nested row/column panes with every-frame-perfect spring animation, drag-to-re-split (with magnetic drop zones), drag-to-resize dividers, focus management, and add-a-panel-from-any-edge — in ~13 KB of vanilla JS, no framework required.

SuperSplit in action
▶ Watch the full-quality video

Inspired by native split-layout frameworks (the kind you'd ship in a terminal like Ghostty). This is a from-scratch web implementation with a CoreAnimation-style spring driving every reflow.

  • 🧩 Tiling tree — arbitrary nesting of row/column splits
  • 🌊 Every-frame-perfect — a critically-damped spring animates every pane's {x, y, w, h} on each frame, so moves/closes/resizes glide
  • 🖱️ Drag to re-split — drag a pane's header onto another pane; a magnetic drop zone shows left/right/top/bottom/center targets
  • ↔️ Drag to resize — grab the gaps between panes
  • Add from any edge — full-height/full-width panels on left/right/top/bottom
  • 🎯 Focus, move, close, swap — full programmatic API + events
  • 💾 Serialize / restore — persist and reload layouts as plain JSON
  • 🎨 Themeable — CSS custom properties, 7 built-in pastel palettes + icons
  • 📦 Zero dependencies — ESM, UMD (browser global) and CommonJS builds

Install

npm install supersplit-js

…or drop it in from a CDN:

<link rel="stylesheet" href="https://unpkg.com/supersplit-js/dist/supersplit.css">
<script src="https://unpkg.com/supersplit-js/dist/supersplit.umd.js"></script>
<!-- window.SuperSplit is now available -->

Quick start

<link rel="stylesheet" href="node_modules/supersplit-js/dist/supersplit.css">
<div id="app" style="width: 100%; height: 100vh;"></div>

<script type="module">
  import SuperSplit from 'supersplit-js';

  const ss = new SuperSplit('#app', {
    layout: {
      type: 'split', dir: 'row', sizes: [2, 1], children: [
        { id: 'editor', title: 'Editor', color: 'blue', icon: 'terminal' },
        { type: 'split', dir: 'col', children: [
          { id: 'preview', title: 'Preview', color: 'green', icon: 'doc' },
          { id: 'console', title: 'Console', color: 'rose',  icon: 'grid' },
        ]},
      ],
    },
  });

  ss.on('change', (layout) => localStorage.setItem('layout', JSON.stringify(layout)));
</script>

The container must have a size — SuperSplit fills it 100% × 100%.

Putting content in panes

Use renderPane to fill each pane's body, or set content per pane (an HTML string or an Element):

const ss = new SuperSplit('#app', {
  layout: { /* … */ },
  renderPane(pane, body, instance) {
    // `body` is the pane's content element
    body.innerHTML = `<iframe src="/panes/${pane.id}"></iframe>`;
  },
});

API

new SuperSplit(container, options?)

container is an element or a selector. Options (all optional):

option default description
layout single pane initial layout spec
gap 9 px gap between sibling panes
padding 13 px padding inside the surface
minPaneSize 64 smallest a pane can be dragged to (px)
paneRadius 11 pane corner radius (px)
headerHeight 37 pane header height (px)
accent #2f7bff focus ring / drop-zone color
spring {stiffness:240, damping:28, mass:1} animation feel
draggable true drag headers to re-split / swap
resizable true drag the gaps to resize
closable true show per-pane close buttons
header true render the pane header bar
renderPane null (pane, bodyEl, instance) => void

Methods

ss.addPane(side, props?)            // 'left' | 'right' | 'top' | 'bottom' → id
ss.splitPane(targetId, side, props?)// split a specific pane → id | null
ss.closePane(id)                    // remove a pane (keeps at least one)
ss.focusPane(id)
ss.movePane(id, dir)                // 'left' | 'right' | 'up' | 'down'
ss.swapPanes(idA, idB)

ss.getFocused()                     // → id | null
ss.getPanes()                       // → [{ id, title, color, icon, data }]
ss.getNeighbor(id, dir)             // → id | null

ss.serialize()                      // → layout spec (JSON-friendly)
ss.load(layout)                     // replace the whole layout
ss.settle()                         // jump all springs to their target
ss.resize()                         // recompute (auto via ResizeObserver)
ss.destroy()                        // remove DOM, observers and listeners

props for new panes: { id?, title?, color?, icon?, content?, data? }.

Events

ss.on('focus',     (id) => {});
ss.on('change',    (layout) => {});   // any structural or size change
ss.on('add',       (pane) => {});
ss.on('close',     (id) => {});
ss.on('split',     ({ id, target, side }) => {});
ss.on('move',      ({ id, dir }) => {});
ss.on('drop',      ({ id, target, side }) => {});
ss.on('resize',    () => {});
ss.on('dragstart', (id) => {});

ss.off(type, fn?) removes a handler (or all handlers for a type).

Layout spec

A tree of split and pane nodes:

// split node
{ type: 'split', dir: 'row' | 'col', sizes?: number[], children: [  ] }

// pane node (leaf)
{ type: 'pane', id?, title?, color?, icon?, content?, data? }

sizes are relative weights (default equal). serialize() emits exactly this shape, so ss.load(ss.serialize()) round-trips.


Theming

All visuals are driven by CSS custom properties on the injected .ss-surface. Set them via options, or override in CSS:

.ss-surface {
  --ss-accent: #ff6f3c;
  --ss-gap: 6px;
  --ss-pane-radius: 8px;
}

Built-in pane palettes (pane.color): green, rose, blue, peach, periwinkle, lavender, indigo, plus the neutral default. Inspect or extend them via SuperSplit.palettes. Built-in pane.icon names live in SuperSplit.icons (sliders, grid, terminal, doc, sidebar, columns, playbox) — or pass any raw SVG string as the icon.


Example app

The examples/ folder contains the full demo from the screenshot — a macOS-style window with a toolbar (move, add-from-edge, close, reset), keyboard shortcuts and layout persistence. Run it locally:

git clone https://github.com/schappim/supersplit-js
cd supersplit-js
npm run build      # generate dist/ (already committed, but just in case)
npm run serve      # → http://localhost:8080  (or: node serve.js, PORT=3000 node serve.js)

serve.js is a tiny zero-dependency static server that sends the correct MIME types. Many simple servers send .css as text/plain (which modern browsers refuse) and mis-serve ES modules — if you hit that, use serve.js.

Build

The library has no build dependencies — build.js is plain Node that emits the ESM, UMD and CommonJS bundles from src/:

npm run build

Browser support

Modern evergreen browsers (Chrome, Safari, Firefox, Edge). Uses Pointer Events, ResizeObserver, the Web Animations API and CSS container queries.

License

MIT © Marcus Schappi

About

A dependency-free tiling split-layout engine for the web — spring-animated panes, drag-to-re-split, drag-to-resize, add-from-any-edge.

Topics

Resources

Stars

Watchers

Forks

Releases

Packages

Used by

Contributors

Languages