Resizable — <mv-resizable>

Horizontal or vertical resizable panels (nestable) separated by handles: pointer drag, arrow keys, min/max sizes in %, double-click collapse with snapping, persisted in localStorage.

CategoryLayout
TypeWeb Component (<mv-resizable>)
Statusstable
Keywordsresizable, split-pane, panels, splitter, layout, ide, persist

When to use

Avoid when

Install

node scripts/add.mjs resizable --out ./src/marvelous

AI agent with the Marvelous UI MCP server: install_components({ slugs: ["resizable"], target_dir: "<absolute path>/src/marvelous", framework: "react" }).

Files copied (dependencies included): tokens/tokens.css, core/base.css, core/dom.js, core/element.js, components/resizable/resizable.js, components/resizable/resizable.css.

Usage

Canonical markup — start from it and customize with attributes, data-* and CSS variables:

<div style="width:100%;height:340px;border:1px solid var(--mv-border);border-radius:var(--mv-radius-xl);overflow:hidden;background:var(--mv-surface);box-shadow:var(--mv-shadow-sm);font-size:.8125rem">
  <mv-resizable id="resizable-demo" storage-key="demo-ide">
    <aside data-size="24" data-min="16" data-max="40" data-collapsible data-label="the explorer" style="background:var(--mv-bg-subtle)">
      <div style="padding:.75rem .9rem .4rem;font-size:.6875rem;font-weight:600;letter-spacing:.06em;text-transform:uppercase;color:var(--mv-fg-muted)">Explorer</div>
      <ul style="margin:0;padding:0 .4rem;list-style:none;display:grid;gap:1px;white-space:nowrap">
        <li style="padding:.3rem .5rem;color:var(--mv-fg-muted)">▾ src</li>
        <li style="padding:.3rem .5rem .3rem 1.4rem;border-radius:var(--mv-radius-sm);background:var(--mv-accent-subtle);color:var(--mv-accent-fg);font-weight:500">invoice.ts</li>
        <li style="padding:.3rem .5rem .3rem 1.4rem">client.ts</li>
        <li style="padding:.3rem .5rem .3rem 1.4rem">tax.ts</li>
        <li style="padding:.3rem .5rem;color:var(--mv-fg-muted)">▸ tests</li>
        <li style="padding:.3rem .5rem">package.json</li>
      </ul>
    </aside>
    <mv-resizable-handle grip></mv-resizable-handle>
    <div data-size="76" data-min="40">
      <mv-resizable direction="vertical">
        <section data-size="68" data-min="30" data-label="the editor" style="display:flex;flex-direction:column">
          <div style="display:flex;align-items:center;gap:.5rem;height:2.25rem;padding:0 .9rem;border-bottom:1px solid var(--mv-border);flex:none"><span style="font-weight:500">invoice.ts</span><span style="color:var(--mv-fg-subtle)">— modified</span></div>
          <pre style="flex:1;margin:0;padding:.75rem .9rem;font:500 .78rem/1.65 var(--mv-font-mono);color:var(--mv-fg-muted);overflow:auto"><span style="color:var(--mv-accent-fg)">export function</span> <span style="color:var(--mv-fg)">totalWithTax</span>(lines: Line[]) {
  <span style="color:var(--mv-accent-fg)">const</span> net = lines.reduce((s, l) =&gt; s + l.price * l.qty, 0);
  <span style="color:var(--mv-accent-fg)">return</span> round(net * (1 + SALES_TAX));
}</pre>
        </section>
        <mv-resizable-handle></mv-resizable-handle>
        <section data-size="32" data-min="15" data-collapsible data-label="the terminal" style="background:var(--mv-bg-subtle)">
          <div style="display:flex;align-items:center;gap:1rem;height:2rem;padding:0 .9rem;border-bottom:1px solid var(--mv-border);font-size:.7rem;font-weight:600;letter-spacing:.05em;text-transform:uppercase;color:var(--mv-fg-muted)"><span style="color:var(--mv-fg)">Terminal</span><span>Problems</span><span>Output</span></div>
          <pre style="margin:0;padding:.6rem .9rem;font:500 .75rem/1.6 var(--mv-font-mono);color:var(--mv-fg-muted)"><span style="color:var(--mv-success)">✓</span> 42 tests passed <span style="color:var(--mv-fg-subtle)">(1.8s)</span>
<span style="color:var(--mv-fg)">~/billing</span> $ </pre>
        </section>
      </mv-resizable>
    </div>
  </mv-resizable>
</div>
<p style="margin:.75rem 0 0;text-align:center;font-size:.75rem;color:var(--mv-fg-muted)">Drag the dividers, use the arrow keys, double-click (or press Enter) to collapse.</p>

API

Attributes

NameTypeDefaultDescription
directionhorizontal | verticalhorizontalSplit axis (vertical: the group takes its parent’s height).
storage-keystringPersists sizes in localStorage (key “mv-resizable:<value>”).
stepnumber5Keyboard step in % (Shift: ×2).
disabledbooleanLocks the sizes.
data-size (panel)numberInitial size in % (the rest is shared).
data-min / data-max (panel)numberBounds in %.
data-collapsible (panel)booleanCollapsible (double-click / Enter on the handle, or drag below half the min).
data-collapsed-size (panel)number0Collapsed size in %.
data-label (panel)stringName used in the handle’s label (“Resize the explorer”).
grip (handle)booleanShows a visible grip in the middle of the handle.

Properties

NameTypeDescription
sizesnumber[]Current sizes in % (setting it = setSizes).
panelsHTMLElement[]Panels (read-only).

Methods

NameDescription
setSizes(sizes, { animate })Applies sizes (normalized to 100).
collapse(index) / expand(index)Collapses or restores a collapsible panel (animated).

Events

NameDescription
mv-resizedetail: { sizes } while resizing.
mv-resize-enddetail: { sizes } on pointer release.

Content structure

NameDescription
childrenPanels (any direct child) separated by <mv-resizable-handle> or [data-handle].

CSS classes

NameDescription
mv-resizable-panel / -handle / -gripParts; data-collapsed on a collapsed panel, data-dragging on the group.

CSS variables

NameDefaultDescription
--mv-resizable-handle-colorvar(--mv-border)Line color.
--mv-resizable-handle-activevar(--mv-accent)Color on hover / focus / drag.

Accessibility

APG “Window Splitter” pattern: each handle is a focusable role=separator, aria-orientation perpendicular to the axis, aria-valuenow/min/max = size in % of the previous panel, aria-controls pointing to that panel, named “Resize <data-label>”. ←/→ (↑/↓ when vertical) adjust, Home/End jump to the bounds, Enter collapses/restores. Enlarged hit area (10 px) for the pointer, touch-action: none for touch.