split-surface
Resizable panel splitter with two layers of progressive enhancement: attribute init and full web component.
Overview
The splitter provides resizable panel support as a two-layer progressive enhancement story — from attribute init to full web component — matching a "platform-first, enhance upward" philosophy.
Layer 1: Attribute Init
Light JS (~80 lines). Apply data-splitter to a container. A small init script injects a full-height drag divider between the first two children with pointer and keyboard events.
Vertical Splitting
Constraints
Set data-min and data-max on children to constrain how far the divider can move.
Keyboard
| Key | Action |
|---|---|
| ArrowLeft / ArrowRight | Move divider 1% |
| Shift + Arrow | Move divider 10% |
| Home | Move to minimum |
| End | Move to maximum |
Layer 2: Web Component
Full component. Self-contained <split-surface> element with persistence, collapsible panels, custom events, and JS API.
All Attributes
| Attribute | Values | Default | Description |
|---|---|---|---|
data-direction | horizontal, vertical | horizontal | Split direction |
data-position | 0-100 | 50 | Initial split position as percentage |
data-min | 0-100 | 10 | Minimum panel size (%) |
data-max | 0-100 | 90 | Maximum panel size (%) |
data-persist | string key | — | localStorage key for position persistence |
data-collapsible | boolean | — | Double-click divider to collapse first panel |
Events
| Event | Detail | Description |
|---|---|---|
split-surface:resize | { position: number } | Fired when the divider position changes. |
split-surface:collapse | { collapsed: boolean } | Fired when panel is collapsed or expanded via double-click. |
JavaScript API
| Property / Method | Type | Description |
|---|---|---|
element.position | number | Get/set current position (0-100) |
element.collapsed | boolean | Get/set collapsed state |
element.reset() | method | Restore to initial position, clear persistence |
Keyboard Navigation
Both layers support the same keyboard controls:
| Key | Action |
|---|---|
| ArrowLeft / ArrowRight | Move divider 1% (horizontal) |
| ArrowUp / ArrowDown | Move divider 1% (vertical) |
| Shift + Arrow | Move divider 10% |
| Home | Move to minimum position |
| End | Move to maximum position |
Accessibility
Separator Role
The divider has role="separator" with aria-orientation, aria-valuenow, aria-valuemin, and aria-valuemax. Screen readers announce the current position.
Keyboard Support
The divider is focusable (tabindex="0") and responds to arrow keys. Shift + arrow provides larger steps. Home and End jump to min/max.
Touch Support
The divider uses touch-action: none and setPointerCapture for reliable pointer drag behavior across devices.
Styling
The divider uses the .split-divider class shared by both layers. The ::after pseudo-element renders a grip indicator.
Progressive Enhancement
Without JavaScript, <split-surface> renders as a simple two-column flex layout. The :not(:defined) selector provides the fallback. Once JS registers the component, the divider appears.
Which Layer to Choose
| Layer | JS | Keyboard | ARIA | Persistence | Best for |
|---|---|---|---|---|---|
data-splitter | ~80 lines | Yes | Yes | No | Enhanced layout containers with accessibility |
<split-surface> | ~120 lines | Yes | Yes | Yes | Full-featured standalone component |
Related
<compare-surface>— Before/after image comparison sliderdata-layout="sidebar"— CSS sidebar layout primitive<tabs-wc>— Tab panels for content switching