Resizable
stimeo--resizable
Dynamic pane splitter leveraging robust PointerCapture dragging and WAI-ARIA APG Splitter keyboard controls.
The stimeo--resizable controller provides an unstyled, headless behavior for fluid resizable layouts (splitters). By employing native PointerCapture (setPointerCapture), it delivers bulletproof drag interactions that never slip even under extreme pointer motions outside the boundaries. Features configurable min/max clamps, double-click collapse/restore, roving keyboard arrow adjustment, and continuous CSS custom property propagation (--stimeo--resizable-fraction) for instant flex or grid rendering.
Left Pane
Primary editor area
Right Pane
Preview workspace
Keyboard
| Key | Action |
|---|---|
| ← / ↑ | Decreases the splitter value by step, shrinking the primary (left/top) pane. |
| → / ↓ | Increases the splitter value by step, expanding the primary (left/top) pane. |
| Home | Instantly snaps the primary pane to its minimum size (minValue). |
| End | Instantly snaps the primary pane to its maximum size (maxValue). |
| Double Click | Double-clicking the handle collapses the pane to its minimum size, or restores it to its previous state. |
<%# Markup for the resizable (splitter / pane-split) demo.
stimeo--resizable provides PointerCapture dragging, a double-click collapse
toggle, and keyboard adjustment, and exposes the ratio (0..1) on the root element
as the CSS variable --stimeo--resizable-fraction. %>
<div class="resizable-demo-container">
<div
class="resizable-demo"
id="resizable-splitter"
data-controller="stimeo--resizable"
data-stimeo--resizable-min-value="20"
data-stimeo--resizable-max-value="80"
data-stimeo--resizable-value-value="50"
data-stimeo--resizable-step-value="5"
style="--stimeo--resizable-fraction: 0.5;"
>
<div
class="resizable-demo__pane resizable-demo__pane--left"
data-stimeo--resizable-target="primary"
>
<div class="resizable-demo__pane-content">
<h3><%= t("components.resizable.demo.pane_left") %></h3>
<p><%= t("components.resizable.demo.pane_left_body") %></p>
</div>
</div>
<div
class="resizable-demo__handle"
role="separator"
tabindex="0"
aria-label="<%= t("components.resizable.demo.splitter_label") %>"
aria-orientation="vertical"
data-stimeo--resizable-target="separator"
data-action="
pointerdown->stimeo--resizable#onPointerDown
keydown->stimeo--resizable#onKeydown
dblclick->stimeo--resizable#toggle
"
>
<div class="resizable-demo__handle-bar" aria-hidden="true"></div>
</div>
<div
class="resizable-demo__pane resizable-demo__pane--right"
data-stimeo--resizable-target="secondary"
>
<div class="resizable-demo__pane-content">
<h3><%= t("components.resizable.demo.pane_right") %></h3>
<p><%= t("components.resizable.demo.pane_right_body") %></p>
</div>
</div>
</div>
<div class="resizable-demo__controls">
<%# demo.js (a module) wires this up with addEventListener. Inline on*
attributes can't reference a module-scoped function, so they aren't used. %>
<button class="resizable-demo__control-btn" type="button" data-resizable-toggle-direction>
<%= t("components.resizable.demo.toggle_direction") %>
</button>
</div>
</div>
/*
* Presentation-only styles for the resizable demo.
* The split ratio is expressed by the Playground as CSS Grid column/row widths using
* the --stimeo--resizable-fraction variable (set on the root by the library).
*/
.resizable-demo-container {
display: flex;
flex-direction: column;
gap: 1rem;
width: 100%;
}
.resizable-demo {
display: grid;
/* Bind the ratio variable to the Grid column. */
grid-template-columns: calc(var(--stimeo--resizable-fraction, 0.5) * 100%) 8px 1fr;
grid-template-rows: 1fr;
width: 100%;
height: 250px;
background: var(--surface-subtle);
border: 1px solid var(--border-default);
border-radius: 12px;
overflow: hidden;
box-shadow: inset 0 2px 4px 0 rgb(0 0 0 / 0.05);
}
/* Vertical-split class toggle. */
.resizable-demo--vertical {
grid-template-columns: 1fr;
/* Bind the ratio variable to the Grid row. */
grid-template-rows: calc(var(--stimeo--resizable-fraction, 0.5) * 100%) 8px 1fr;
}
.resizable-demo__pane {
background: var(--surface-card);
overflow: hidden;
}
.resizable-demo__pane--left {
background: var(--surface-card);
}
.resizable-demo__pane--right {
background: var(--surface-subtle);
}
.resizable-demo__pane-content {
padding: 1.25rem;
height: 100%;
display: flex;
flex-direction: column;
justify-content: center;
}
.resizable-demo__pane-content h3 {
margin: 0 0 0.5rem 0;
font-size: 1rem;
color: var(--fg);
}
.resizable-demo__pane-content p {
margin: 0;
font-size: 0.875rem;
color: var(--color-text-muted);
}
/* Splitter handle. */
.resizable-demo__handle {
display: flex;
align-items: center;
justify-content: center;
background: var(--border-strong);
cursor: col-resize;
position: relative;
transition: background 0.15s ease;
outline: none;
}
.resizable-demo--vertical .resizable-demo__handle {
cursor: row-resize;
}
.resizable-demo__handle:hover,
.resizable-demo__handle:focus {
background: var(--accent);
}
.resizable-demo__handle-bar {
width: 2px;
height: 24px;
background: var(--surface-card);
border-radius: 1px;
}
.resizable-demo--vertical .resizable-demo__handle-bar {
width: 24px;
height: 2px;
}
.resizable-demo__controls {
display: flex;
justify-content: flex-end;
}
.resizable-demo__control-btn {
padding: 0.5rem 1rem;
font-size: 0.875rem;
font-weight: 500;
border: 1px solid var(--border-strong);
border-radius: 6px;
background: var(--surface-card);
color: var(--fg);
cursor: pointer;
transition: all 0.15s ease;
}
.resizable-demo__control-btn:hover {
background: var(--surface-subtle);
border-color: var(--border-interactive);
}
// Demo script that toggles the split layout direction (horizontal / vertical).
// This file loads as a module (type="module"), so top-level functions are not
// global — wire interactions with addEventListener, not an inline onclick.
const toggleButton = document.querySelector("[data-resizable-toggle-direction]");
toggleButton?.addEventListener("click", () => {
const el = document.getElementById("resizable-splitter");
if (!el) return;
const isVertical = el.classList.toggle("resizable-demo--vertical");
const separator = el.querySelector('[role="separator"]');
if (separator) {
// Horizontal split (column layout): the divider is "vertical".
// Vertical split (row layout): the divider is "horizontal".
separator.setAttribute("aria-orientation", isVertical ? "horizontal" : "vertical");
}
});
These demo styles use shared design tokens (light + dark). Copy the shared styles too, then toggle data-theme on your root element for dark mode.
The data-* attributes you add to your own HTML to wire this component. Put the data-controller below on a root element, then place its targets / values / actions inside that element.
On the root element
data-controller="stimeo--resizable"
Targets
| Name | Description | Attribute |
|---|---|---|
primary
required
|
The first (primary) pane sized by the split fraction. | data-stimeo--resizable-target="primary" |
secondary
required
|
The second (secondary) pane filling the remaining space. | data-stimeo--resizable-target="secondary" |
separator
required
|
The draggable splitter handle (role=separator) carrying the aria-value* state. |
data-stimeo--resizable-target="separator" |
Values
| Name | Description | Attribute |
|---|---|---|
min
|
The minimum split percentage (default 0). | data-stimeo--resizable-min-value |
max
|
The maximum split percentage (default 100). | data-stimeo--resizable-max-value |
step
|
The percentage step for arrow-key adjustments (default 1). | data-stimeo--resizable-step-value |
value
|
The current split percentage, clamped to min/max (default 50). | data-stimeo--resizable-value-value |
Actions
| Name | Description | Action |
|---|---|---|
onKeydown
|
Adjusts the split with Arrow keys (per orientation), Home/End to min/max, Enter to toggle. | stimeo--resizable#onKeydown |
onPointerDown
|
Begins a pointer drag of the separator with pointer capture and focus. | stimeo--resizable#onPointerDown |
toggle
|
Collapses the pane to min or restores it to the previous/max size. | stimeo--resizable#toggle |
Events
| Name | Description | Event |
|---|---|---|
change
|
Dispatched when the split value changes; detail carries value and fraction. | stimeo--resizable:change |
State hooks
The library only manages these ARIA/data attributes and custom properties. Your CSS reads them to render the look — selectors like [aria-selected], [aria-expanded], or var(--stimeo--…) hook into this state.
| Hook | Target | Meaning |
|---|---|---|
aria-valuenow |
splitter handle target | Stores the current primary pane percentage width/height (0 to 100). |
aria-valuemin |
splitter handle target | Minimum percentage size allowed for the primary pane. |
aria-valuemax |
splitter handle target | Maximum percentage size allowed for the primary pane. |
--stimeo--resizable-fraction |
resizable container | Exposes the active ratio as a float fraction ("0.0" to "1.0") ideal for standard CSS Grid layout configurations. |