Skip to main content

VerticalSegments

N-pane resizable horizontal layout with draggable dividers. Drop in any number of pane contents and get a flex container with smooth drag-to-resize separators, keyboard support, and configurable min-width clamping.

Quick start
import { VerticalSegments } from "mates-ui";

VerticalSegments(
  [html`<p>Left</p>`, html`<p>Center</p>`, html`<p>Right</p>`],
  { defaultWidths: ["220px", "40%", "auto"] },
);

Two panes — equal split

import { html, renderApp } from 'mates';
import { VerticalSegments } from 'mates-ui';

const App = () => () => html`
<div style="height:300px;width:100%;max-width:40rem;border:1px solid var(--md-color-border);border-radius:var(--md-radius-md);overflow:hidden;">
${VerticalSegments([
html`<div style="padding:var(--md-space-4);height:100%;box-sizing:border-box;"><p style="margin:0;font-weight:600;font-size:var(--md-text-sm);">Left pane</p><p style="margin:var(--md-space-2) 0 0;font-size:var(--md-text-sm);color:var(--md-color-text-muted);">Drag the divider to resize.</p></div>`,
html`<div style="padding:var(--md-space-4);height:100%;box-sizing:border-box;"><p style="margin:0;font-weight:600;font-size:var(--md-text-sm);">Right pane</p><p style="margin:var(--md-space-2) 0 0;font-size:var(--md-text-sm);color:var(--md-color-text-muted);">Both panes start at equal widths.</p></div>`,
])}
</div>
`;

renderApp(App, document.getElementById('app'));

Three panes — custom widths

import { html, renderApp } from 'mates';
import { VerticalSegments } from 'mates-ui';

const App = () => () => html`
<div style="height:320px;width:100%;max-width:40rem;border:1px solid var(--md-color-border);border-radius:var(--md-radius-md);overflow:hidden;">
${VerticalSegments([
html`<div style="padding:var(--md-space-4);height:100%;box-sizing:border-box;"><p style="margin:0;font-weight:600;font-size:var(--md-text-sm);">Navigation</p><p style="margin:var(--md-space-2) 0 0;font-size:var(--md-text-sm);color:var(--md-color-text-muted);">220 px starting width.</p></div>`,
html`<div style="padding:var(--md-space-4);height:100%;box-sizing:border-box;"><p style="margin:0;font-weight:600;font-size:var(--md-text-sm);">Editor</p><p style="margin:var(--md-space-2) 0 0;font-size:var(--md-text-sm);color:var(--md-color-text-muted);">40% starting width.</p></div>`,
html`<div style="padding:var(--md-space-4);height:100%;box-sizing:border-box;"><p style="margin:0;font-weight:600;font-size:var(--md-text-sm);">Preview</p><p style="margin:var(--md-space-2) 0 0;font-size:var(--md-text-sm);color:var(--md-color-text-muted);">&quot;auto&quot; — remaining space.</p></div>`,
], { defaultWidths: ["220px", "40%", "auto"] })}
</div>
`;

renderApp(App, document.getElementById('app'));

No handle

import { html, renderApp } from 'mates';
import { VerticalSegments } from 'mates-ui';

const App = () => () => html`
<div style="height:240px;width:100%;max-width:40rem;border:1px solid var(--md-color-border);border-radius:var(--md-radius-md);overflow:hidden;">
${VerticalSegments([
html`<div style="padding:var(--md-space-4);height:100%;box-sizing:border-box;"><p style="margin:0;font-weight:600;font-size:var(--md-text-sm);">Left pane</p><p style="margin:var(--md-space-2) 0 0;font-size:var(--md-text-sm);color:var(--md-color-text-muted);">No grip dots — plain divider line.</p></div>`,
html`<div style="padding:var(--md-space-4);height:100%;box-sizing:border-box;"><p style="margin:0;font-weight:600;font-size:var(--md-text-sm);">Right pane</p><p style="margin:var(--md-space-2) 0 0;font-size:var(--md-text-sm);color:var(--md-color-text-muted);">Clean divider, no affordance.</p></div>`,
], { showHandle: false })}
</div>
`;

renderApp(App, document.getElementById('app'));

Minimum pane width

import { html, renderApp } from 'mates';
import { VerticalSegments } from 'mates-ui';

const App = () => () => html`
<div style="height:280px;width:100%;max-width:40rem;border:1px solid var(--md-color-border);border-radius:var(--md-radius-md);overflow:hidden;">
${VerticalSegments([
html`<div style="padding:var(--md-space-4);height:100%;box-sizing:border-box;"><p style="margin:0;font-weight:600;font-size:var(--md-text-sm);">Pane A</p><p style="margin:var(--md-space-2) 0 0;font-size:var(--md-text-sm);color:var(--md-color-text-muted);">Min 120 px — won't collapse below.</p></div>`,
html`<div style="padding:var(--md-space-4);height:100%;box-sizing:border-box;"><p style="margin:0;font-weight:600;font-size:var(--md-text-sm);">Pane B</p><p style="margin:var(--md-space-2) 0 0;font-size:var(--md-text-sm);color:var(--md-color-text-muted);">Also clamped at 120 px.</p></div>`,
html`<div style="padding:var(--md-space-4);height:100%;box-sizing:border-box;"><p style="margin:0;font-weight:600;font-size:var(--md-text-sm);">Pane C</p><p style="margin:var(--md-space-2) 0 0;font-size:var(--md-text-sm);color:var(--md-color-text-muted);">Same floor for every pane.</p></div>`,
], { minPaneWidth: "120px" })}
</div>
`;

renderApp(App, document.getElementById('app'));

Single pane — no divider

import { html, renderApp } from 'mates';
import { VerticalSegments } from 'mates-ui';

const App = () => () => html`
<div style="height:160px;width:100%;max-width:40rem;border:1px solid var(--md-color-border);border-radius:var(--md-radius-md);overflow:hidden;">
${VerticalSegments([
html`<div style="padding:var(--md-space-4);height:100%;box-sizing:border-box;"><p style="margin:0;font-weight:600;font-size:var(--md-text-sm);">Single pane — no separator rendered</p><p style="margin:var(--md-space-2) 0 0;font-size:var(--md-text-sm);color:var(--md-color-text-muted);">One child = plain flex container with no dividers.</p></div>`,
])}
</div>
`;

renderApp(App, document.getElementById('app'));

VerticalSegments

import { VerticalSegments } from "mates-ui";
Prop Type Default Description
ariaLabel string "Resizable panes" Accessible label for the container's role="group".
attr AttrMap HTML attributes on the root element.
children* TemplateResult[] Array of pane contents. One child renders a plain container; N children render N−1 draggable dividers.
classes string | string[] Extra root classes — joined and appended after built-in classes.
data DataAttrMap Root `data-*` map (`testId` → `data-testid`). `testId` prop / `attr` win over `data`.
defaultWidths string[] [] Per-pane initial widths. Supports px ("200px"), percent ("30%"), or "auto" (equal share). Shorter arrays are padded with "auto".
id string Root `id` shorthand (`attr.id` wins). Form native-control `id` props are unchanged.
minPaneWidth string "48px" Minimum resize floor for every pane.
on OnEventMap DOM event handlers on the root element.
paneLabel string "Pane {n}" Per-pane aria-label template; `{n}` is replaced with the 1-based pane number.
resizePaneLabel string "Resize pane {n}" Separator aria-label template; `{n}` is replaced with the pane number.
showHandle boolean true Show the 3-dot grip handle on each divider.
step number 8 Keyboard ArrowLeft / ArrowRight step size in pixels.
style StyleMap Inline CSS on the root element.

CSS tokens

Token Role
--md-color-border Divider resting color.
--md-color-primary Divider hover / drag / focus color.
--md-color-text-muted Grip handle dot color.

Caveats

  • Each pane must manage its own overflow — add `overflow: auto` to content inside panes as needed.
  • The last pane always fills remaining space (`flex: 1 1 0`) and cannot be directly resized — drag the separator to its left.
  • `defaultWidths` are resolved once on mount relative to the container's pixel width.