Resizable panels
Lab Available in release assets
Resize two authored regions with bounded pointer and keyboard mechanics while preserving their native reading order and stacked fallback.
Resizable panels primary example
<section data-enhance="resizable-panels" aria-label="Editor panels">
<section id="source-panel" data-panel>
<h2>Source</h2>
<p>Authored first in reading order and fully usable without JavaScript.</p>
</section>
<div role="separator" tabindex="0" aria-label="Resize source and preview" aria-controls="source-panel preview-panel" aria-orientation="vertical" aria-valuemin="20" aria-valuemax="80" aria-valuenow="50" aria-valuetext="50% for primary panel">
</div>
<section id="preview-panel" data-panel>
<h2>Preview</h2>
<p>Authored second in reading order.</p>
</section>
</section>Show code
<section data-enhance="resizable-panels" aria-label="Editor panels">
<section id="source-panel" data-panel>
<h2>Source</h2>
<p>Authored first in reading order and fully usable without JavaScript.</p>
</section>
<div role="separator" tabindex="0" aria-label="Resize source and preview" aria-controls="source-panel preview-panel" aria-orientation="vertical" aria-valuemin="20" aria-valuemax="80" aria-valuenow="50" aria-valuetext="50% for primary panel">
</div>
<section id="preview-panel" data-panel>
<h2>Preview</h2>
<p>Authored second in reading order.</p>
</section>
</section>Variant gallery
Each authored example uses the production assets and includes copyable markup.
Inline split
Explicit enhancement enables a wide-container splitter. Arrow keys move 2%, Shift moves 10%, and Home/End select 20%/80%. Narrow or unenhanced panels stack.
<div style="min-inline-size: 0; overflow-wrap: anywhere">
<section id="inline-split" data-enhance="resizable-panels" aria-label="Source and preview panels">
<section id="inline-split-source" data-panel tabindex="0" aria-labelledby="inline-split-source-title">
<h2 id="inline-split-source-title">Source</h2>
<p>First in document order. Panel size is temporary and is never saved by this behavior.</p>
<label for="inline-split-note">Source note</label>
<textarea id="inline-split-note" style="box-sizing: border-box; inline-size: 100%; min-inline-size: 0">An editable source note.</textarea>
</section>
<div role="separator" tabindex="0" aria-label="Resize source and preview" aria-controls="inline-split-source inline-split-preview" aria-orientation="vertical" aria-valuemin="20" aria-valuemax="80" aria-valuenow="50" aria-valuetext="50% for primary panel">
</div>
<section id="inline-split-preview" data-panel tabindex="0" aria-labelledby="inline-split-preview-title">
<h2 id="inline-split-preview-title">Preview</h2>
<p>Second in document order. <a href="/layouts/stack/">Read about layouts</a>.</p>
</section>
</section>
</div>
<script type="module">import { enhanceResizablePanels } from "/labs.js"; enhanceResizablePanels(document);</script>Show code
<div style="min-inline-size: 0; overflow-wrap: anywhere">
<section id="inline-split" data-enhance="resizable-panels" aria-label="Source and preview panels">
<section id="inline-split-source" data-panel tabindex="0" aria-labelledby="inline-split-source-title">
<h2 id="inline-split-source-title">Source</h2>
<p>First in document order. Panel size is temporary and is never saved by this behavior.</p>
<label for="inline-split-note">Source note</label>
<textarea id="inline-split-note" style="box-sizing: border-box; inline-size: 100%; min-inline-size: 0">An editable source note.</textarea>
</section>
<div role="separator" tabindex="0" aria-label="Resize source and preview" aria-controls="inline-split-source inline-split-preview" aria-orientation="vertical" aria-valuemin="20" aria-valuemax="80" aria-valuenow="50" aria-valuetext="50% for primary panel">
</div>
<section id="inline-split-preview" data-panel tabindex="0" aria-labelledby="inline-split-preview-title">
<h2 id="inline-split-preview-title">Preview</h2>
<p>Second in document order. <a href="/layouts/stack/">Read about layouts</a>.</p>
</section>
</section>
</div>
<script type="module">import { enhanceResizablePanels } from "/labs.js"; enhanceResizablePanels(document);</script>Block split
A horizontal separator resizes rows with Up/Down. This application authors a definite 24rem height only in the enhanced wide layout; long content scrolls within focusable panels.
<div style="min-inline-size: 0; overflow-wrap: anywhere">
<section id="block-split" data-enhance="resizable-panels" aria-label="Source and preview panels">
<section id="block-split-source" data-panel tabindex="0" aria-labelledby="block-split-source-title">
<h2 id="block-split-source-title">Source</h2>
<p>First in document order. Panel size is temporary and is never saved by this behavior.</p>
<label for="block-split-note">Source note</label>
<textarea id="block-split-note" style="box-sizing: border-box; inline-size: 100%; min-inline-size: 0">An editable source note.</textarea>
<p>Long source content remains readable and scrollable within its own panel. Long source content remains readable and scrollable within its own panel. Long source content remains readable and scrollable within its own panel. Long source content remains readable and scrollable within its own panel. Long source content remains readable and scrollable within its own panel. Long source content remains readable and scrollable within its own panel. Long source content remains readable and scrollable within its own panel. Long source content remains readable and scrollable within its own panel. Long source content remains readable and scrollable within its own panel. Long source content remains readable and scrollable within its own panel. Long source content remains readable and scrollable within its own panel. Long source content remains readable and scrollable within its own panel.</p>
</section>
<div role="separator" tabindex="0" aria-label="Resize source and preview" aria-controls="block-split-source block-split-preview" aria-orientation="horizontal" aria-valuemin="20" aria-valuemax="80" aria-valuenow="50" aria-valuetext="50% for primary panel">
</div>
<section id="block-split-preview" data-panel tabindex="0" aria-labelledby="block-split-preview-title">
<h2 id="block-split-preview-title">Preview</h2>
<p>Second in document order. <a href="/layouts/stack/">Read about layouts</a>.</p>
<p>Vorschau: lange zusammenhängende Inhalte bleiben im Bereich lesbar. Vorschau: lange zusammenhängende Inhalte bleiben im Bereich lesbar. Vorschau: lange zusammenhängende Inhalte bleiben im Bereich lesbar. Vorschau: lange zusammenhängende Inhalte bleiben im Bereich lesbar. Vorschau: lange zusammenhängende Inhalte bleiben im Bereich lesbar. Vorschau: lange zusammenhängende Inhalte bleiben im Bereich lesbar. Vorschau: lange zusammenhängende Inhalte bleiben im Bereich lesbar. Vorschau: lange zusammenhängende Inhalte bleiben im Bereich lesbar. Vorschau: lange zusammenhängende Inhalte bleiben im Bereich lesbar. Vorschau: lange zusammenhängende Inhalte bleiben im Bereich lesbar. Vorschau: lange zusammenhängende Inhalte bleiben im Bereich lesbar. Vorschau: lange zusammenhängende Inhalte bleiben im Bereich lesbar.</p>
</section>
</section>
</div>
<style>@container thin-resizable-panels (min-width: 40rem) { #block-split[data-resizable-enhanced] { block-size: 24rem; } }</style>
<script type="module">import { enhanceResizablePanels } from "/labs.js"; enhanceResizablePanels(document);</script>Show code
<div style="min-inline-size: 0; overflow-wrap: anywhere">
<section id="block-split" data-enhance="resizable-panels" aria-label="Source and preview panels">
<section id="block-split-source" data-panel tabindex="0" aria-labelledby="block-split-source-title">
<h2 id="block-split-source-title">Source</h2>
<p>First in document order. Panel size is temporary and is never saved by this behavior.</p>
<label for="block-split-note">Source note</label>
<textarea id="block-split-note" style="box-sizing: border-box; inline-size: 100%; min-inline-size: 0">An editable source note.</textarea>
<p>Long source content remains readable and scrollable within its own panel. Long source content remains readable and scrollable within its own panel. Long source content remains readable and scrollable within its own panel. Long source content remains readable and scrollable within its own panel. Long source content remains readable and scrollable within its own panel. Long source content remains readable and scrollable within its own panel. Long source content remains readable and scrollable within its own panel. Long source content remains readable and scrollable within its own panel. Long source content remains readable and scrollable within its own panel. Long source content remains readable and scrollable within its own panel. Long source content remains readable and scrollable within its own panel. Long source content remains readable and scrollable within its own panel.</p>
</section>
<div role="separator" tabindex="0" aria-label="Resize source and preview" aria-controls="block-split-source block-split-preview" aria-orientation="horizontal" aria-valuemin="20" aria-valuemax="80" aria-valuenow="50" aria-valuetext="50% for primary panel">
</div>
<section id="block-split-preview" data-panel tabindex="0" aria-labelledby="block-split-preview-title">
<h2 id="block-split-preview-title">Preview</h2>
<p>Second in document order. <a href="/layouts/stack/">Read about layouts</a>.</p>
<p>Vorschau: lange zusammenhängende Inhalte bleiben im Bereich lesbar. Vorschau: lange zusammenhängende Inhalte bleiben im Bereich lesbar. Vorschau: lange zusammenhängende Inhalte bleiben im Bereich lesbar. Vorschau: lange zusammenhängende Inhalte bleiben im Bereich lesbar. Vorschau: lange zusammenhängende Inhalte bleiben im Bereich lesbar. Vorschau: lange zusammenhängende Inhalte bleiben im Bereich lesbar. Vorschau: lange zusammenhängende Inhalte bleiben im Bereich lesbar. Vorschau: lange zusammenhängende Inhalte bleiben im Bereich lesbar. Vorschau: lange zusammenhängende Inhalte bleiben im Bereich lesbar. Vorschau: lange zusammenhängende Inhalte bleiben im Bereich lesbar. Vorschau: lange zusammenhängende Inhalte bleiben im Bereich lesbar. Vorschau: lange zusammenhängende Inhalte bleiben im Bereich lesbar.</p>
</section>
</section>
</div>
<style>@container thin-resizable-panels (min-width: 40rem) { #block-split[data-resizable-enhanced] { block-size: 24rem; } }</style>
<script type="module">import { enhanceResizablePanels } from "/labs.js"; enhanceResizablePanels(document);</script>RTL inline split
The primary source panel starts at the RTL inline edge. Left grows it and Right shrinks it; DOM reading order stays Source then Preview.
<div dir="rtl" style="min-inline-size: 0; overflow-wrap: anywhere">
<section id="rtl-split" data-enhance="resizable-panels" aria-label="Source and preview panels">
<section id="rtl-split-source" data-panel tabindex="0" aria-labelledby="rtl-split-source-title">
<h2 id="rtl-split-source-title">Source</h2>
<p>First in document order. Panel size is temporary and is never saved by this behavior.</p>
<label for="rtl-split-note">Source note</label>
<textarea id="rtl-split-note" style="box-sizing: border-box; inline-size: 100%; min-inline-size: 0">An editable source note.</textarea>
</section>
<div role="separator" tabindex="0" aria-label="Resize source and preview" aria-controls="rtl-split-source rtl-split-preview" aria-orientation="vertical" aria-valuemin="20" aria-valuemax="80" aria-valuenow="50" aria-valuetext="50% for primary panel">
</div>
<section id="rtl-split-preview" data-panel tabindex="0" aria-labelledby="rtl-split-preview-title">
<h2 id="rtl-split-preview-title">Preview</h2>
<p>Second in document order. <a href="/layouts/stack/">Read about layouts</a>.</p>
</section>
</section>
</div>
<script type="module">import { enhanceResizablePanels } from "/labs.js"; enhanceResizablePanels(document);</script>Show code
<div dir="rtl" style="min-inline-size: 0; overflow-wrap: anywhere">
<section id="rtl-split" data-enhance="resizable-panels" aria-label="Source and preview panels">
<section id="rtl-split-source" data-panel tabindex="0" aria-labelledby="rtl-split-source-title">
<h2 id="rtl-split-source-title">Source</h2>
<p>First in document order. Panel size is temporary and is never saved by this behavior.</p>
<label for="rtl-split-note">Source note</label>
<textarea id="rtl-split-note" style="box-sizing: border-box; inline-size: 100%; min-inline-size: 0">An editable source note.</textarea>
</section>
<div role="separator" tabindex="0" aria-label="Resize source and preview" aria-controls="rtl-split-source rtl-split-preview" aria-orientation="vertical" aria-valuemin="20" aria-valuemax="80" aria-valuenow="50" aria-valuetext="50% for primary panel">
</div>
<section id="rtl-split-preview" data-panel tabindex="0" aria-labelledby="rtl-split-preview-title">
<h2 id="rtl-split-preview-title">Preview</h2>
<p>Second in document order. <a href="/layouts/stack/">Read about layouts</a>.</p>
</section>
</section>
</div>
<script type="module">import { enhanceResizablePanels } from "/labs.js"; enhanceResizablePanels(document);</script>Anatomy
- Labelled group
- Two authored regions
- Focusable ARIA separator
- Minimum/current/maximum values
Variants and sizes
- Inline split — shown above
- Block split — shown above
- RTL — shown above
States and difficult content
Rendered fixtures exercise authored content and constrained layouts with the same production assets.
Default, keyboard and pointer resizing
Focus the wide-layout separator and use arrows or drag its captured pointer. The ARIA value and visual split update together. No size is persisted.
<div style="min-inline-size: 0; overflow-wrap: anywhere">
<section id="default-split" data-enhance="resizable-panels" aria-label="Source and preview panels">
<section id="default-split-source" data-panel tabindex="0" aria-labelledby="default-split-source-title">
<h2 id="default-split-source-title">Source</h2>
<p>First in document order. Panel size is temporary and is never saved by this behavior.</p>
<label for="default-split-note">Source note</label>
<textarea id="default-split-note" style="box-sizing: border-box; inline-size: 100%; min-inline-size: 0">An editable source note.</textarea>
</section>
<div role="separator" tabindex="0" aria-label="Resize source and preview" aria-controls="default-split-source default-split-preview" aria-orientation="vertical" aria-valuemin="20" aria-valuemax="80" aria-valuenow="50" aria-valuetext="50% for primary panel">
</div>
<section id="default-split-preview" data-panel tabindex="0" aria-labelledby="default-split-preview-title">
<h2 id="default-split-preview-title">Preview</h2>
<p>Second in document order. <a href="/layouts/stack/">Read about layouts</a>.</p>
</section>
</section>
</div>
<script type="module">import { enhanceResizablePanels } from "/labs.js"; enhanceResizablePanels(document);</script>Show code
<div style="min-inline-size: 0; overflow-wrap: anywhere">
<section id="default-split" data-enhance="resizable-panels" aria-label="Source and preview panels">
<section id="default-split-source" data-panel tabindex="0" aria-labelledby="default-split-source-title">
<h2 id="default-split-source-title">Source</h2>
<p>First in document order. Panel size is temporary and is never saved by this behavior.</p>
<label for="default-split-note">Source note</label>
<textarea id="default-split-note" style="box-sizing: border-box; inline-size: 100%; min-inline-size: 0">An editable source note.</textarea>
</section>
<div role="separator" tabindex="0" aria-label="Resize source and preview" aria-controls="default-split-source default-split-preview" aria-orientation="vertical" aria-valuemin="20" aria-valuemax="80" aria-valuenow="50" aria-valuetext="50% for primary panel">
</div>
<section id="default-split-preview" data-panel tabindex="0" aria-labelledby="default-split-preview-title">
<h2 id="default-split-preview-title">Preview</h2>
<p>Second in document order. <a href="/layouts/stack/">Read about layouts</a>.</p>
</section>
</section>
</div>
<script type="module">import { enhanceResizablePanels } from "/labs.js"; enhanceResizablePanels(document);</script>Minimum primary panel
The authored minimum is 20%. Home reaches it; further shrinking stops at that bound.
<div style="min-inline-size: 0; overflow-wrap: anywhere">
<section id="minimum-split" data-enhance="resizable-panels" aria-label="Source and preview panels">
<section id="minimum-split-source" data-panel tabindex="0" aria-labelledby="minimum-split-source-title">
<h2 id="minimum-split-source-title">Source</h2>
<p>First in document order. Panel size is temporary and is never saved by this behavior.</p>
<label for="minimum-split-note">Source note</label>
<textarea id="minimum-split-note" style="box-sizing: border-box; inline-size: 100%; min-inline-size: 0">An editable source note.</textarea>
</section>
<div role="separator" tabindex="0" aria-label="Resize source and preview" aria-controls="minimum-split-source minimum-split-preview" aria-orientation="vertical" aria-valuemin="20" aria-valuemax="80" aria-valuenow="20" aria-valuetext="20% for primary panel">
</div>
<section id="minimum-split-preview" data-panel tabindex="0" aria-labelledby="minimum-split-preview-title">
<h2 id="minimum-split-preview-title">Preview</h2>
<p>Second in document order. <a href="/layouts/stack/">Read about layouts</a>.</p>
</section>
</section>
</div>
<script type="module">import { enhanceResizablePanels } from "/labs.js"; enhanceResizablePanels(document);</script>Show code
<div style="min-inline-size: 0; overflow-wrap: anywhere">
<section id="minimum-split" data-enhance="resizable-panels" aria-label="Source and preview panels">
<section id="minimum-split-source" data-panel tabindex="0" aria-labelledby="minimum-split-source-title">
<h2 id="minimum-split-source-title">Source</h2>
<p>First in document order. Panel size is temporary and is never saved by this behavior.</p>
<label for="minimum-split-note">Source note</label>
<textarea id="minimum-split-note" style="box-sizing: border-box; inline-size: 100%; min-inline-size: 0">An editable source note.</textarea>
</section>
<div role="separator" tabindex="0" aria-label="Resize source and preview" aria-controls="minimum-split-source minimum-split-preview" aria-orientation="vertical" aria-valuemin="20" aria-valuemax="80" aria-valuenow="20" aria-valuetext="20% for primary panel">
</div>
<section id="minimum-split-preview" data-panel tabindex="0" aria-labelledby="minimum-split-preview-title">
<h2 id="minimum-split-preview-title">Preview</h2>
<p>Second in document order. <a href="/layouts/stack/">Read about layouts</a>.</p>
</section>
</section>
</div>
<script type="module">import { enhanceResizablePanels } from "/labs.js"; enhanceResizablePanels(document);</script>Maximum primary panel
The authored maximum is 80%. End reaches it; further growth stops at that bound.
<div style="min-inline-size: 0; overflow-wrap: anywhere">
<section id="maximum-split" data-enhance="resizable-panels" aria-label="Source and preview panels">
<section id="maximum-split-source" data-panel tabindex="0" aria-labelledby="maximum-split-source-title">
<h2 id="maximum-split-source-title">Source</h2>
<p>First in document order. Panel size is temporary and is never saved by this behavior.</p>
<label for="maximum-split-note">Source note</label>
<textarea id="maximum-split-note" style="box-sizing: border-box; inline-size: 100%; min-inline-size: 0">An editable source note.</textarea>
</section>
<div role="separator" tabindex="0" aria-label="Resize source and preview" aria-controls="maximum-split-source maximum-split-preview" aria-orientation="vertical" aria-valuemin="20" aria-valuemax="80" aria-valuenow="80" aria-valuetext="80% for primary panel">
</div>
<section id="maximum-split-preview" data-panel tabindex="0" aria-labelledby="maximum-split-preview-title">
<h2 id="maximum-split-preview-title">Preview</h2>
<p>Second in document order. <a href="/layouts/stack/">Read about layouts</a>.</p>
</section>
</section>
</div>
<script type="module">import { enhanceResizablePanels } from "/labs.js"; enhanceResizablePanels(document);</script>Show code
<div style="min-inline-size: 0; overflow-wrap: anywhere">
<section id="maximum-split" data-enhance="resizable-panels" aria-label="Source and preview panels">
<section id="maximum-split-source" data-panel tabindex="0" aria-labelledby="maximum-split-source-title">
<h2 id="maximum-split-source-title">Source</h2>
<p>First in document order. Panel size is temporary and is never saved by this behavior.</p>
<label for="maximum-split-note">Source note</label>
<textarea id="maximum-split-note" style="box-sizing: border-box; inline-size: 100%; min-inline-size: 0">An editable source note.</textarea>
</section>
<div role="separator" tabindex="0" aria-label="Resize source and preview" aria-controls="maximum-split-source maximum-split-preview" aria-orientation="vertical" aria-valuemin="20" aria-valuemax="80" aria-valuenow="80" aria-valuetext="80% for primary panel">
</div>
<section id="maximum-split-preview" data-panel tabindex="0" aria-labelledby="maximum-split-preview-title">
<h2 id="maximum-split-preview-title">Preview</h2>
<p>Second in document order. <a href="/layouts/stack/">Read about layouts</a>.</p>
</section>
</section>
</div>
<script type="module">import { enhanceResizablePanels } from "/labs.js"; enhanceResizablePanels(document);</script>RTL resizing
Try physical Left/Right and pointer movement from the RTL start edge. Screen-reader and touch review remain human Lab evidence.
<div dir="rtl" style="min-inline-size: 0; overflow-wrap: anywhere">
<section id="rtl-state" data-enhance="resizable-panels" aria-label="Source and preview panels">
<section id="rtl-state-source" data-panel tabindex="0" aria-labelledby="rtl-state-source-title">
<h2 id="rtl-state-source-title">Source</h2>
<p>First in document order. Panel size is temporary and is never saved by this behavior.</p>
<label for="rtl-state-note">Source note</label>
<textarea id="rtl-state-note" style="box-sizing: border-box; inline-size: 100%; min-inline-size: 0">An editable source note.</textarea>
</section>
<div role="separator" tabindex="0" aria-label="Resize source and preview" aria-controls="rtl-state-source rtl-state-preview" aria-orientation="vertical" aria-valuemin="20" aria-valuemax="80" aria-valuenow="50" aria-valuetext="50% for primary panel">
</div>
<section id="rtl-state-preview" data-panel tabindex="0" aria-labelledby="rtl-state-preview-title">
<h2 id="rtl-state-preview-title">Preview</h2>
<p>Second in document order. <a href="/layouts/stack/">Read about layouts</a>.</p>
</section>
</section>
</div>
<script type="module">import { enhanceResizablePanels } from "/labs.js"; enhanceResizablePanels(document);</script>Show code
<div dir="rtl" style="min-inline-size: 0; overflow-wrap: anywhere">
<section id="rtl-state" data-enhance="resizable-panels" aria-label="Source and preview panels">
<section id="rtl-state-source" data-panel tabindex="0" aria-labelledby="rtl-state-source-title">
<h2 id="rtl-state-source-title">Source</h2>
<p>First in document order. Panel size is temporary and is never saved by this behavior.</p>
<label for="rtl-state-note">Source note</label>
<textarea id="rtl-state-note" style="box-sizing: border-box; inline-size: 100%; min-inline-size: 0">An editable source note.</textarea>
</section>
<div role="separator" tabindex="0" aria-label="Resize source and preview" aria-controls="rtl-state-source rtl-state-preview" aria-orientation="vertical" aria-valuemin="20" aria-valuemax="80" aria-valuenow="50" aria-valuetext="50% for primary panel">
</div>
<section id="rtl-state-preview" data-panel tabindex="0" aria-labelledby="rtl-state-preview-title">
<h2 id="rtl-state-preview-title">Preview</h2>
<p>Second in document order. <a href="/layouts/stack/">Read about layouts</a>.</p>
</section>
</section>
</div>
<script type="module">import { enhanceResizablePanels } from "/labs.js"; enhanceResizablePanels(document);</script>Long content and narrow fallback
At narrow widths or enlarged text, panels stack and the resize separator is hidden. Long source and translated preview text remain usable. Disable scripts to inspect the same reading order at wide widths.
<div style="min-inline-size: 0; overflow-wrap: anywhere">
<section id="long-split" data-enhance="resizable-panels" aria-label="Source and preview panels">
<section id="long-split-source" data-panel tabindex="0" aria-labelledby="long-split-source-title">
<h2 id="long-split-source-title">Source</h2>
<p>First in document order. Panel size is temporary and is never saved by this behavior.</p>
<label for="long-split-note">Source note</label>
<textarea id="long-split-note" style="box-sizing: border-box; inline-size: 100%; min-inline-size: 0">An editable source note.</textarea>
<p>Long source content remains readable and scrollable within its own panel. Long source content remains readable and scrollable within its own panel. Long source content remains readable and scrollable within its own panel. Long source content remains readable and scrollable within its own panel. Long source content remains readable and scrollable within its own panel. Long source content remains readable and scrollable within its own panel. Long source content remains readable and scrollable within its own panel. Long source content remains readable and scrollable within its own panel. Long source content remains readable and scrollable within its own panel. Long source content remains readable and scrollable within its own panel. Long source content remains readable and scrollable within its own panel. Long source content remains readable and scrollable within its own panel.</p>
</section>
<div role="separator" tabindex="0" aria-label="Resize source and preview" aria-controls="long-split-source long-split-preview" aria-orientation="vertical" aria-valuemin="20" aria-valuemax="80" aria-valuenow="50" aria-valuetext="50% for primary panel">
</div>
<section id="long-split-preview" data-panel tabindex="0" aria-labelledby="long-split-preview-title">
<h2 id="long-split-preview-title">Preview</h2>
<p>Second in document order. <a href="/layouts/stack/">Read about layouts</a>.</p>
<p>Vorschau: lange zusammenhängende Inhalte bleiben im Bereich lesbar. Vorschau: lange zusammenhängende Inhalte bleiben im Bereich lesbar. Vorschau: lange zusammenhängende Inhalte bleiben im Bereich lesbar. Vorschau: lange zusammenhängende Inhalte bleiben im Bereich lesbar. Vorschau: lange zusammenhängende Inhalte bleiben im Bereich lesbar. Vorschau: lange zusammenhängende Inhalte bleiben im Bereich lesbar. Vorschau: lange zusammenhängende Inhalte bleiben im Bereich lesbar. Vorschau: lange zusammenhängende Inhalte bleiben im Bereich lesbar. Vorschau: lange zusammenhängende Inhalte bleiben im Bereich lesbar. Vorschau: lange zusammenhängende Inhalte bleiben im Bereich lesbar. Vorschau: lange zusammenhängende Inhalte bleiben im Bereich lesbar. Vorschau: lange zusammenhängende Inhalte bleiben im Bereich lesbar.</p>
</section>
</section>
</div>
<script type="module">import { enhanceResizablePanels } from "/labs.js"; enhanceResizablePanels(document);</script>Show code
<div style="min-inline-size: 0; overflow-wrap: anywhere">
<section id="long-split" data-enhance="resizable-panels" aria-label="Source and preview panels">
<section id="long-split-source" data-panel tabindex="0" aria-labelledby="long-split-source-title">
<h2 id="long-split-source-title">Source</h2>
<p>First in document order. Panel size is temporary and is never saved by this behavior.</p>
<label for="long-split-note">Source note</label>
<textarea id="long-split-note" style="box-sizing: border-box; inline-size: 100%; min-inline-size: 0">An editable source note.</textarea>
<p>Long source content remains readable and scrollable within its own panel. Long source content remains readable and scrollable within its own panel. Long source content remains readable and scrollable within its own panel. Long source content remains readable and scrollable within its own panel. Long source content remains readable and scrollable within its own panel. Long source content remains readable and scrollable within its own panel. Long source content remains readable and scrollable within its own panel. Long source content remains readable and scrollable within its own panel. Long source content remains readable and scrollable within its own panel. Long source content remains readable and scrollable within its own panel. Long source content remains readable and scrollable within its own panel. Long source content remains readable and scrollable within its own panel.</p>
</section>
<div role="separator" tabindex="0" aria-label="Resize source and preview" aria-controls="long-split-source long-split-preview" aria-orientation="vertical" aria-valuemin="20" aria-valuemax="80" aria-valuenow="50" aria-valuetext="50% for primary panel">
</div>
<section id="long-split-preview" data-panel tabindex="0" aria-labelledby="long-split-preview-title">
<h2 id="long-split-preview-title">Preview</h2>
<p>Second in document order. <a href="/layouts/stack/">Read about layouts</a>.</p>
<p>Vorschau: lange zusammenhängende Inhalte bleiben im Bereich lesbar. Vorschau: lange zusammenhängende Inhalte bleiben im Bereich lesbar. Vorschau: lange zusammenhängende Inhalte bleiben im Bereich lesbar. Vorschau: lange zusammenhängende Inhalte bleiben im Bereich lesbar. Vorschau: lange zusammenhängende Inhalte bleiben im Bereich lesbar. Vorschau: lange zusammenhängende Inhalte bleiben im Bereich lesbar. Vorschau: lange zusammenhängende Inhalte bleiben im Bereich lesbar. Vorschau: lange zusammenhängende Inhalte bleiben im Bereich lesbar. Vorschau: lange zusammenhängende Inhalte bleiben im Bereich lesbar. Vorschau: lange zusammenhängende Inhalte bleiben im Bereich lesbar. Vorschau: lange zusammenhängende Inhalte bleiben im Bereich lesbar. Vorschau: lange zusammenhängende Inhalte bleiben im Bereich lesbar.</p>
</section>
</section>
</div>
<script type="module">import { enhanceResizablePanels } from "/labs.js"; enhanceResizablePanels(document);</script>Disconnect, reconnect and replace
Disconnect removes interaction and restores stacked layout. Reconnect retains the authored current value. Replace clones the separator, calls refresh(), and explicitly restores focus in the application handler; core does not infer focus for a removed node.
<div style="min-inline-size: 0; overflow-wrap: anywhere">
<section id="lifecycle-split" data-enhance="resizable-panels" aria-label="Source and preview panels">
<section id="lifecycle-split-source" data-panel tabindex="0" aria-labelledby="lifecycle-split-source-title">
<h2 id="lifecycle-split-source-title">Source</h2>
<p>First in document order. Panel size is temporary and is never saved by this behavior.</p>
<label for="lifecycle-split-note">Source note</label>
<textarea id="lifecycle-split-note" style="box-sizing: border-box; inline-size: 100%; min-inline-size: 0">An editable source note.</textarea>
</section>
<div role="separator" tabindex="0" aria-label="Resize source and preview" aria-controls="lifecycle-split-source lifecycle-split-preview" aria-orientation="vertical" aria-valuemin="20" aria-valuemax="80" aria-valuenow="50" aria-valuetext="50% for primary panel">
</div>
<section id="lifecycle-split-preview" data-panel tabindex="0" aria-labelledby="lifecycle-split-preview-title">
<h2 id="lifecycle-split-preview-title">Preview</h2>
<p>Second in document order. <a href="/layouts/stack/">Read about layouts</a>.</p>
</section>
</section>
</div>
<div style="display: flex; flex-wrap: wrap; gap: .5rem; margin-block: 1rem; min-inline-size: 0">
<button id="lifecycle-split-action-0" type="button" style="box-sizing: border-box; max-inline-size: 100%; min-inline-size: 0; min-block-size: 2.75rem; white-space: normal; padding-inline: .5rem; overflow-wrap: anywhere">Disconnect</button>
<button id="lifecycle-split-action-1" type="button" style="box-sizing: border-box; max-inline-size: 100%; min-inline-size: 0; min-block-size: 2.75rem; white-space: normal; padding-inline: .5rem; overflow-wrap: anywhere">Reconnect</button>
<button id="lifecycle-split-action-2" type="button" style="box-sizing: border-box; max-inline-size: 100%; min-inline-size: 0; min-block-size: 2.75rem; white-space: normal; padding-inline: .5rem; overflow-wrap: anywhere">Replace separator</button>
</div>
<p id="lifecycle-split-status" role="status" style="overflow-wrap: anywhere">Connected.</p>
<script type="module">import { enhanceResizablePanels } from "/labs.js"; const root = document.getElementById("lifecycle-split"); const [controller] = enhanceResizablePanels(root); const status = document.getElementById("lifecycle-split-status"); document.getElementById("lifecycle-split-action-0").addEventListener("click", () => { controller.disconnect(); status.textContent = "Disconnected: panels stack."; }); document.getElementById("lifecycle-split-action-1").addEventListener("click", () => { controller.connect(root); status.textContent = "Reconnected: current value retained."; }); document.getElementById("lifecycle-split-action-2").addEventListener("click", () => { controller.connect(root); const old = root.querySelector('[role="separator"]'); old.replaceWith(old.cloneNode(true)); controller.refresh(); root.querySelector('[role="separator"]').focus(); status.textContent = "Separator replaced; application restored focus."; });</script>Show code
<div style="min-inline-size: 0; overflow-wrap: anywhere">
<section id="lifecycle-split" data-enhance="resizable-panels" aria-label="Source and preview panels">
<section id="lifecycle-split-source" data-panel tabindex="0" aria-labelledby="lifecycle-split-source-title">
<h2 id="lifecycle-split-source-title">Source</h2>
<p>First in document order. Panel size is temporary and is never saved by this behavior.</p>
<label for="lifecycle-split-note">Source note</label>
<textarea id="lifecycle-split-note" style="box-sizing: border-box; inline-size: 100%; min-inline-size: 0">An editable source note.</textarea>
</section>
<div role="separator" tabindex="0" aria-label="Resize source and preview" aria-controls="lifecycle-split-source lifecycle-split-preview" aria-orientation="vertical" aria-valuemin="20" aria-valuemax="80" aria-valuenow="50" aria-valuetext="50% for primary panel">
</div>
<section id="lifecycle-split-preview" data-panel tabindex="0" aria-labelledby="lifecycle-split-preview-title">
<h2 id="lifecycle-split-preview-title">Preview</h2>
<p>Second in document order. <a href="/layouts/stack/">Read about layouts</a>.</p>
</section>
</section>
</div>
<div style="display: flex; flex-wrap: wrap; gap: .5rem; margin-block: 1rem; min-inline-size: 0">
<button id="lifecycle-split-action-0" type="button" style="box-sizing: border-box; max-inline-size: 100%; min-inline-size: 0; min-block-size: 2.75rem; white-space: normal; padding-inline: .5rem; overflow-wrap: anywhere">Disconnect</button>
<button id="lifecycle-split-action-1" type="button" style="box-sizing: border-box; max-inline-size: 100%; min-inline-size: 0; min-block-size: 2.75rem; white-space: normal; padding-inline: .5rem; overflow-wrap: anywhere">Reconnect</button>
<button id="lifecycle-split-action-2" type="button" style="box-sizing: border-box; max-inline-size: 100%; min-inline-size: 0; min-block-size: 2.75rem; white-space: normal; padding-inline: .5rem; overflow-wrap: anywhere">Replace separator</button>
</div>
<p id="lifecycle-split-status" role="status" style="overflow-wrap: anywhere">Connected.</p>
<script type="module">import { enhanceResizablePanels } from "/labs.js"; const root = document.getElementById("lifecycle-split"); const [controller] = enhanceResizablePanels(root); const status = document.getElementById("lifecycle-split-status"); document.getElementById("lifecycle-split-action-0").addEventListener("click", () => { controller.disconnect(); status.textContent = "Disconnected: panels stack."; }); document.getElementById("lifecycle-split-action-1").addEventListener("click", () => { controller.connect(root); status.textContent = "Reconnected: current value retained."; }); document.getElementById("lifecycle-split-action-2").addEventListener("click", () => { controller.connect(root); const old = root.querySelector('[role="separator"]'); old.replaceWith(old.cloneNode(true)); controller.refresh(); root.querySelector('[role="separator"]').focus(); status.textContent = "Separator replaced; application restored focus."; });</script>- Default — shown above
- Minimum — shown above
- Maximum — shown above
- Pointer dragging — shown above
- Keyboard resizing — shown above
- RTL — shown above
- Narrow stacked fallback — shown above
- Disconnected — shown above
- Reconnected — shown above
- Morphed separator — shown above
- Long content — shown above
Interaction and keyboard behavior
Arrow keys change the logical primary size by 2%; Shift uses 10%; Home and End select authored bounds. Pointer capture is limited to the separator and released on up, cancel, or disconnect. refresh() reacquires authored elements after a morph. Persistence is explicitly outside core.
Accessibility
Use role=separator with tabindex, aria-controls, orientation, value min/max/now, and a human-readable value text. Regions remain in DOM reading order. Keep Lab until browser, screen-reader, zoom, pointer, touch, and RTL evidence supports promotion.
- Contract
SPLITTER· APG pattern- Basis
- APG Window Splitter as an explicitly experimental mapping; APG review/example work is incomplete
- Behavior guarantees
- An experimental Window Splitter: one named separator controls exactly two contained panels, or the behavior fails closed and the stacked reading order stays authoritative.
- Normalizes orientation and bounded values, ignores disabled separators, and releases pointer capture on cancellation or disconnect.
- Rebinds a replaced separator on refresh().
- Evidence
tests/unit/library/resizable-panels.test.tstests/unit/library/advanced-component-contracts.test.ts
API
Add data-enhance=resizable-panels to the authored group and role=separator between exactly two regions. Optional --thin-panel-size starts at aria-valuenow. Call refresh() after server morphs; disconnect() before removal. Do not persist sizing in core.
Behavior tier: dependency-free
Custom properties
--thin-panel-size50%Initial logical size of the primary panel before interaction.Fallback
Without JavaScript the two authored regions stack in normal reading order and the separator is visually inert.
Assets and dependencies
/labs.css/labs.js
Dependencies: None
Source: packages/ui/src/items/resizable-panels/style.ts