A splitter must change both the pane’s visible width and the divider’s announced value. This example derives them from one bounded state. Keyboard resizing and pointer dragging update that same value; Enter collapses the pane and restores its remembered width.
Bind the value to a responsive pane layout
This splitter divides the available pane space into proportions, so the example shrinks with its container. Its minimum is deliberately nonzero: collapse means the smallest permitted share rather than hiding navigation completely. The accessible value text makes that unit explicit.
import { RefSubject } from "@typed/fx";
import { html, component } from "@typed/template";
import * as WindowSplitter from "@typed/ui/WindowSplitter";
export const ResizableInspector = component(function* (id: string) {
const state = yield* WindowSplitter.makeState({
value: 35, min: 15, max: 70, step: 5, orientation: "vertical",
});
// Reserve the handle width, then share the remaining space between panes.
const layout = RefSubject.map(state, ({ value }) =>
`display: grid; grid-template-columns: minmax(0, ${value}fr) 12px minmax(0, ${100 - value}fr);`);
return html`<section>
<p id=${`${id}-help`}>Drag the divider, or focus it and use Left/Right. Enter collapses or restores.</p>
<div style=${layout}>
<aside id=${`${id}-pane`} style="overflow-wrap: anywhere;">
<h2>Inspector</h2><p>Selected project properties.</p>
</aside>
${WindowSplitter.WindowSplitter({ state, primaryPaneId: `${id}-pane`, label: "Inspector width",
valueText: RefSubject.map(state, ({ value }) => `${value}% of pane space`),
props: { "aria-describedby": `${id}-help`, style: "cursor: col-resize; background: currentColor;" },
})}
<div style="overflow-wrap: anywhere;"><h2>Project content</h2></div>
</div>
</section>`;
});
Pass a stable, page-unique ID. The separator’s primaryPaneId points to the aside from this instance.
State drives both grid proportions and aria-valuenow; valueText supplies readable units. The
default drag scale treats the space excluding the handle as 100 units, matching these grid tracks.
minmax(0, …) lets both tracks shrink instead of overflowing at their content’s intrinsic width.
Keep a visible focus indicator and check real content at narrow widths; complex pane contents may
need a stacked layout when neither pane has enough usable room.
Understand orientation and collapse memory
Orientation describes the separator line, not the direction the pane grows. A vertical separator
uses Left/Right; a horizontal separator uses Up/Down. Arrows apply step, Home selects min, End
selects max, and Enter calls toggleCollapsed. The separator remains a normal tab stop. When its
rendered aria-disabled is true, keyboard and pointer handlers do not change state. Disabling during a drag ends that drag
on its next pointer movement.
setValue and adjust clamp to the configured range. toggleCollapsed records the current value
in previousValue when collapsing to min, and restores that recorded value when already at min.
Ordinary resizing does not update the restore value. That distinction matters if another control
jumps to min: Enter then restores the previously recorded collapse value, not necessarily the most
recent width before that jump.
The constructor validates finite structural values through its schema but does not enforce the
relationships min <= max or step > 0. Supply those semantic constraints yourself. A zero step
makes arrow presses inert; a negative step reverses conventional movement. If viewport changes
alter available space, update constraints and clamp the current size coherently rather than
exposing an impossible layout range.
Translate pointer movement into the same value
The component captures the primary pointer on a left-button/touch press, keeping movement attached
to the separator even when the pointer crosses into either pane. Vertical separators use horizontal
movement; horizontal separators use vertical movement. The gesture starts from the current value,
so grabbing an edge of a thick handle does not snap the layout to the pointer’s absolute position.
Every movement goes through setValue, retaining the same min/max clamp as keyboard changes.
Without valuePerPixel, the parent dimension minus separator thickness represents 100 value units.
This fits the default percentage range and a two-pane grid using value and 100 - value fractional
tracks. For a pixel-based pane layout, pass valuePerPixel: 1 instead. Other units need a positive,
finite scale. Scale and orientation are sampled at gesture start. Positive axis-aligned CSS scaling
is supported; rotation, skew, or reversed visual layouts need a separate coordinate policy. See the
WindowSplitter API for custom drag scaling.
Pointer up, cancellation, lost capture, and render-Scope teardown release the session. A second
pointer cannot take over an active gesture. The host composes caller styles with touch-action: none
so touch movement remains a resize gesture rather than page scrolling. This uses the normal reactive
style binding and preserves the caller’s other style declarations. Keep the full supplied props on
the actual separator when overriding its host.
The component does not persist preferred widths or set pane CSS itself. Persist a chosen value at an application boundary if needed; keep ARIA and visible layout derived from that same value.
The APG window splitter pattern describes
the focusable separator interaction. MDN’s separator role reference
distinguishes a static separator from an adjustable one. This component uses role=separator with
range and controls attributes; a decorative Separator is not interchangeable with it.
Validate the layout and the announced range together
Check pane width and aria-valuenow together after keys and pointer drags. Verify clamping,
collapse/restore, disabled behavior, and removal during a drag. If only one output changes, look for
a missing style subscription or a second sizing state bypassing the family.
The splitter does not need a collection and has no selected-versus-active item distinction: its value is a continuous layout choice and its focus is the actual separator. Keep that simpler model instead of importing a roving registry intended for multi-item widgets. Public API: WindowSplitter.