Resizable
stimeo--resizable
Drag a divider to resize the areas either side of it, and do the same from the keyboard.
Splits an area and lets you drag the divider to resize what sits either side. During a drag the pointer stays captured, so leaving the divider or even the window never drops it. You can set how far it may narrow and widen. Double-clicking the divider collapses and restores it. The keyboard does the same, stepping with the arrow keys and jumping to the ends with Home and End. The current split is readable from CSS, so the markup and the layout stay entirely yours.
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. |
| F6 | Cycle focus through the panes (wrapping). |
<%# 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"
id="resizable-demo-primary"
data-stimeo--resizable-target="primary"
>
<div class="resizable-demo__pane-content">
<h3 id="resizable-demo-primary-label">
<%= 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"
<%# Window Splitter names the splitter after the primary pane. That pane
has a visible heading here, so the pattern asks for aria-labelledby
rather than a hand-written aria-label. %>
aria-labelledby="resizable-demo-primary-label"
aria-controls="resizable-demo-primary"
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="demo-trigger 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;
}
.resizable-demo--vertical .resizable-demo__handle {
cursor: row-resize;
}
.resizable-demo__handle:hover,
.resizable-demo__handle:focus {
background: var(--accent);
}
.resizable-demo__handle:focus-visible {
outline: 2px solid var(--accent);
outline-offset: 2px;
}
.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;
}
/* Only the smaller, lighter label is the demo's own; the look is the shared launcher. */
.resizable-demo__control-btn {
font-size: 0.875rem;
font-weight: 500;
}
// 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). A declaration that cannot be read falls back to it. | data-stimeo--resizable-min-value |
max
|
The maximum split percentage (default 100). A maximum below the minimum collapses onto it. | 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 puts it back where it was (opening fully only when no previous position is known). | 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. |