Resizable container with two or more panes and draggable gutters. Drag to resize, double-click a gutter to collapse / restore, and use arrow keys (or Home / End) when a gutter is focused. Sizes can be constrained per pane and persisted to localStorage.
Horizontal split — side-by-side, vertical gutter
Sidebar + main content. Start pane is constrained to min: 200px
and max: 60%, with a default of 280px. Size persists
to localStorage under the id demo-horizontal.
Files
- 📁 components/
- 📁 utils/
- 📄 main.scss
- 📄 README.md
- 📄 package.json
- 📄 .gitignore
- 📄 LICENSE
main.scss
Drag the gutter, double-click it to collapse, or focus it and use arrow keys.
@use 'variables/index' as *;
@use 'core' as *;
@use 'utilities' as *;
:root {
@include output-base-css-variables;
@include output-pa-css-variables;
}
Vertical split — stacked, horizontal gutter
Editor over console. Constraints: min: 80px, max: 80%,
default 60%. Persists under demo-vertical.
Editor
function greet(name) {
return `Hello, ${name}!`;
}
console.log(greet('Pure Admin'));
Spaced cards — gutter with breathing room
Use native gap on the splitter root to add space between the panes
and the gutter — the JS subtracts it from the available space so percent
constraints stay accurate. A thicker gutter is opt-in via
--pa-splitter-gutter-size.
Left card
This pane holds a card. The flex gap on the splitter keeps
the card from touching the gutter — no per-pane padding hack needed.
Right card
Drag the gutter — both cards reflow. The 10px gutter is set inline via
--pa-splitter-gutter-size; the default is 6px.
Minimize to rail — vertical header instead of collapse
Opt in with is_minimizable on the start pane. The gutter's collapse
action (double-click, Enter/Space, the toggle button in
the header, or pressing the gutter while minimized) toggles the start pane to a
thin rail with a vertical card title — instead of disappearing. Click the rail
or press the gutter to restore. Rail width is configurable via rail_size
(default 40px). You can also just drag the gutter inward — once
the requested width drops below 40% of the minimized side's natural min
(configurable via minimize_threshold) it snaps to rail; dragging
back outward across the same threshold pops it back to min.
Bonus: the Editor card's actions use the new
actions_variant="responsive" pattern — drag the gutter rightward to
shrink the Editor pane, and once the header drops below
$card-actions-collapse-at (28rem ≈ 280px) the spread buttons
collapse into a single split button. Pure CSS container query, no JS observer.
File explorer
- 📁 src/
- 📁 dist/
- 📄 main.scss
- 📄 splitter.js
- 📄 README.md
Click the chevron in the header to minimize. Click the rail (or press the gutter) to restore.
Editor
When the file explorer minimizes, this card stays put and the gutter slides next to the rail.
// Toggle the minimize state: // - double-click the gutter // - focus gutter + press Enter / Space // - click the rail (while minimized) // - press the gutter (while minimized)
Progressive overflow — buttons drop into a "More" menu one at a time
A second collapse model, complementing --responsive. Mark the
actions wrapper with actions_variant="overflow" and the
PureAdminCardActionsOverflow hook measures each button on init.
A ResizeObserver watches the wrapper; when the row can't fit, the
lowest-priority button moves into a "..." menu (appended to <body>
so card overflow: hidden can't clip it).
- Priority via
data-pa-actions-priority="N"— higher stays longer. -
Tiebreak direction via
data-pa-actions-overflow-from="end"(default — rightmost drops first) or"start"(leftmost drops first).
Editor
Shrink me — buttons collapse into the "..." menu by ascending priority.
Run has data-pa-actions-priority="10" and stays last.
Drag the gutter left/right to shrink and grow the editor card. Watch the action row in its header.
Minimize end pane to rail — right-edge inspector pattern
Set is_minimizable on the LAST pane to collapse it to a
vertical-title rail on the right edge. Same restore paths as the start variant
(rail click, gutter press, toggle button, dblclick, Enter/Space,
or drag the gutter outward past the threshold). Useful for inspector / detail /
properties panels.
Editor
Drag the gutter to the right (shrinking the inspector) — once the inspector's width drops below 75% of its implied min, it snaps to a rail on the right edge.
// The inspector on the right is the // rail-side here. Press the rail, the // gutter, or the toggle button to // restore it.
Inspector
Properties, outlines, references, etc. live here. Click the chevron, double-click the gutter, or drag the gutter rightward to collapse.
N panes — pick a count, both edges minimizable
N-pane markup uses per-pane size / min /
max attrs instead of root-level min-start /
max-start. The first and last panes opt into rail collapse with
is_minimizable. Middle panes can also collapse (post-edge-only
restriction lift in 2.9.0-rc03). Drag any gutter and either neighbour will snap
to rail below the threshold. Each pane count remembers its own layout in
localStorage.
Sidebar
- 📁 src/
- 📁 demo/
- 📄 main.scss
- 📄 README.md
Workspace 1
Workspace 1 of 1
// Pane 2
console.log("hi");
Inspector
- Type
- function
- Args
- name: string
- Returns
- string
localStorage persistence
Both demos above persist their size under pa-splitter:<id>.
Reload the page — your drag positions stick. Clear them with the button below.
Markup — two panes (legacy shorthand)
<div class="pa-splitter pa-splitter--horizontal"
data-pa-splitter
data-pa-splitter-id="my-id"
data-pa-splitter-min-start="200px"
data-pa-splitter-max-start="60%"
data-pa-splitter-default="280px">
<div class="pa-splitter__pane
pa-splitter__pane--start">A</div>
<div class="pa-splitter__gutter"
role="separator"
aria-orientation="vertical"
tabindex="0"></div>
<div class="pa-splitter__pane
pa-splitter__pane--end">B</div>
</div>
Triggered by the --start / --end modifiers. The
end pane fills the leftover via flex: 1; only the start pane
carries an explicit size. The Phoenix wrapper always emits the N-pane form
below (upstream normalises legacy markup into N-pane at init anyway).
Markup — N panes
<div class="pa-splitter pa-splitter--horizontal"
data-pa-splitter
data-pa-splitter-id="my-id">
<div class="pa-splitter__pane"
data-pa-splitter-size="240px"
data-pa-splitter-min="180px"
data-pa-splitter-minimize>A</div>
<div class="pa-splitter__gutter"
role="separator" tabindex="0"></div>
<div class="pa-splitter__pane"
data-pa-splitter-min="240px">B</div>
<div class="pa-splitter__gutter"
role="separator" tabindex="0"></div>
<div class="pa-splitter__pane"
data-pa-splitter-size="280px"
data-pa-splitter-minimize>C</div>
</div>
Any N ≥ 2. Panes and gutters must alternate. Per-pane
data-pa-splitter-size / -min / -max
replace the root-level -start attributes. Only pane 0 and pane
N-1 honour data-pa-splitter-minimize (middle panes can collapse
too as of 2.9.0-rc03 — slack tunnels through the rail wall). Panes without
an explicit size share leftover space equally.
Phoenix wrapper
<.splitter
id="my-id"
orientation="horizontal"
style="height: 360px;"
>
<:pane size="240px" min="180px" is_minimizable>
A
</:pane>
<:pane min="240px">
B
</:pane>
<:pane size="280px" is_minimizable>
C
</:pane>
</.splitter>
<.splitter> emits the N-pane HTML above and wires the
PureAdminSplitter hook when id is set. Gutters
are inserted automatically between panes.
Data attributes
On root (both modes):
| Attribute | Default | Description |
|---|---|---|
data-pa-splitter
|
—
| Marker. Required. |
data-pa-splitter-id
|
none
| Enables persistence under pa-splitter:<id> |
data-pa-splitter-step
|
10
| Keyboard step in px |
data-pa-splitter-rail-size
|
40
| Rail width in px (minimized state) |
data-pa-splitter-minimize-threshold
|
0.40
| Drag snaps to rail below this fraction of the minimized side's natural min |
On root (legacy 2-pane only):
| Attribute | Default | Description |
|---|---|---|
data-pa-splitter-min-start
|
0
| px or % |
data-pa-splitter-max-start
|
available
| px or % |
data-pa-splitter-default
|
30%
| Initial size if no saved state |
data-pa-splitter-minimize
|
off
| "start" or "end" opts into rail collapse on that side |
On each pane (N-pane only):
| Attribute | Default | Description |
|---|---|---|
data-pa-splitter-size
|
shared
| Initial size (px or %). Unsized panes split the leftover. |
data-pa-splitter-min
|
0
| px or % |
data-pa-splitter-max
|
available
| px or % |
data-pa-splitter-minimize
|
—
| Marker. Only honoured on the first and last panes. |
Keyboard
| Key | Action |
|---|---|
| ← ↑ | Move boundary toward start (shrink left pane) |
| → ↓ | Move boundary toward end (grow left pane) |
| Home | Jump left pane to its minimum |
| End | Jump left pane to its maximum |
| Enter / Space | Toggle minimize on the nearest minimizable neighbour |
Each gutter handles its own keyboard input. Tab focuses the next gutter; the "left pane" is whichever pane sits before the focused gutter.
JavaScript API
Auto-initializes on [data-pa-splitter] at
DOMContentLoaded. The Phoenix wrapper's
PureAdminSplitter hook also calls init() on
mount + update for LiveView-patched markup. For manually inserted splitters:
// Init a single element
PaSplitter.init(document.getElementById('my-splitter'));
// Init all uninitialized splitters in a subtree
PaSplitter.initAll(someContainer);
init() is idempotent — calling it twice on the same element
is a no-op.