v1.0.0

<bmx-splitter>

Two panes and a divider the reader can move.

14 properties · 2 events · 3 methods · 5 parts

Example

Drag the divider, or focus it with Tab and use the arrow keys. Double-click it to fold the list away and again to bring it back at the size it had.

Messages

A list narrow enough to be useful and wide enough to read. It will not go below 140 pixels, and past 60 more it folds away entirely.

Quarterly review

The pane that takes what is left. Drag towards the middle and the divider is pulled onto the halfway mark — the position a reader is usually aiming at, and a surprisingly hard one to hit by hand.

The list has 34% of the room.
Show markup
<div class="row">
  <p style="margin: 0">
    Drag the divider, or focus it with Tab and use the arrow keys. Double-click it to fold the list away and again to
    bring it back at the size it had.
  </p>
</div>

<div class="row">
  <bmx-splitter
    id="ex-splitter"
    position="34%"
    start-min="140"
    end-min="200"
    collapsible="start"
    collapse-at="60"
    snap="50%"
    style="block-size: 220px; border: 1px solid var(--bmx-border); border-radius: var(--bmx-radius-md); overflow: hidden"
  >
    <div slot="start" style="padding: 0.75rem">
      <strong>Messages</strong>
      <p style="margin: 0.5rem 0 0; color: var(--bmx-text-subtle)">
        A list narrow enough to be useful and wide enough to read. It will not go below 140 pixels, and past 60 more it
        folds away entirely.
      </p>
    </div>
    <div slot="end" style="padding: 0.75rem">
      <strong>Quarterly review</strong>
      <p style="margin: 0.5rem 0 0; color: var(--bmx-text-subtle)">
        The pane that takes what is left. Drag towards the middle and the divider is pulled onto the halfway mark — the
        position a reader is usually aiming at, and a surprisingly hard one to hit by hand.
      </p>
    </div>
  </bmx-splitter>
</div>

<div class="row">
  <span class="note" id="ex-splitter-out">The list has 34% of the room.</span>
</div>

<script type="module">
  await customElements.whenDefined('bmx-splitter');

  const out = document.getElementById('ex-splitter-out');

  /*
   * `bmxInput` fires all through the drag and `bmxChange` only when it is let
   * go, which is the same pair `bmx-slider` uses: draw from the first, save on
   * the second. Writing a layout preference to a server on every pixel of a
   * drag is the version of this people ship by accident.
   */
  document.getElementById('ex-splitter').addEventListener('bmxInput', event => {
    const { fraction, collapsed } = event.detail;

    out.textContent =
      collapsed === 'start'
        ? 'The list is folded away. Double-click the divider, or press Enter on it, to bring it back.'
        : `The list has ${Math.round(fraction * 100)}% of the room.`;
  });
</script>
<bmx-splitter position="30%" start-min="180" end-min="320">
  <nav slot="start">…the list…</nav>
  <article slot="end">…the thing selected in it…</article>
</bmx-splitter>

WHERE THE WORK IS

In src/core/splitter.ts, which is arithmetic and has no DOM in it: what a legal position is, what happens when the two minimums cannot both fit, where a snap point pulls to, and what an arrow key does in each of the four combinations of orientation and writing direction. Every bug people meet in a splitter - a pane that will not reach its minimum, a divider that jumps on the first pixel of a drag, a collapse that cannot be undone, arrows that go the wrong way in Arabic - is one of those, and none of them needs a browser to find. What is left here is the pointer, the keyboard wiring and the ARIA.

THE DRAG IS POINTER CAPTURE, LIKE bmx-slider

Capture keeps the drag alive when the pointer leaves the divider - past the end of the panel, out of the window, over an iframe - and releases itself if the pointer is lost. A pair of listeners on the document does neither, and the iframe is the case that bites: without capture, dragging a divider over an embedded map hands the drag to the map.

WHAT A SCREEN READER IS TOLD

The divider is a separator with a tabindex, which is what makes it a focusable separator - a window splitter rather than a decorative rule - and it carries aria-valuenow as a percentage so that "40" means something without knowing how wide the container is. aria-controls points at the pane it resizes, so the relationship is stated rather than implied by position.

Properties

PropertyAttributeTypeDefaultDescription
collapseAt collapse-at number How far past its minimum a pane must be dragged before it folds away. Off by default, and deliberately: a pane that vanishes when a drag overshoots - and can only be brought back by finding a divider now flush against an edge - is a worse control than one that simply stops. Set it, and the pane collapses; Enter on the divider then folds and unfolds it, restoring the size it had rather than a default.
collapsible collapsible 'none' | 'start' | 'end' | 'both' 'none' Which pane may collapse.
disabled disabled boolean false The divider cannot be moved.
endMax end-max string | number The most it will take.
endMin end-min string | number 0 The least room the second pane will take.
label label string 'Resize panes' The divider's accessible name.
largeStep large-step number How far Shift and an arrow key move it. Defaults to five steps.
orientation orientation BmxSplitOrientation 'horizontal' Which way the panes are stacked. vertical puts the first pane on top.
position position string | number '50%' How much room the first pane gets: a length, a percentage, or a number of pixels. Reflected as it moves, so a page that wants to remember where a reader put the divider can read it off the element and write it back next time.
snap snap (string | number)[] | string '' Positions the divider is pulled towards, as lengths or percentages. A comma-separated or JSON attribute, or an array in JavaScript. 50% alone is a middle the divider settles into; a list gives it several. See src/core/markup.ts for the attribute spellings.
snapTolerance snap-tolerance number 12 How near a snap point the divider must be to be pulled onto it.
startMax start-max string | number The most it will take.
startMin start-min string | number 0 The least room the first pane will take.
step step number 16 How far one arrow key press moves the divider.

Events

EventDetailDescription
bmxChange BmxSplitterChangeDetail Fired when the divider is let go, or moved by a key.
bmxInput BmxSplitterChangeDetail Fired continuously while the divider is being dragged.

Methods

MethodSignatureDescription
collapse collapse(pane?: "start" | "end") => Promise<void> Fold a pane away.
expand expand() => Promise<void> Bring a folded pane back to the size it had.
setPosition setPosition(position: string | number) => Promise<void> Put the divider somewhere. Clamped, and snapped.

Slots

SlotDescription
end The second pane.
start The first pane: the left one, or the top one, and the right one under RTL.

CSS shadow parts

PartDescription
base The container the two panes divide.
divider The bar between them.
end The second pane.
handle The grip drawn on the divider.
start The first pane.

CSS custom properties

PropertyDescription
--bmx-splitter-color The divider's colour.
--bmx-splitter-color-hover Its colour under the pointer, and while it is being dragged.
--bmx-splitter-handle-color The grip's colour.
--bmx-splitter-handle-size The length of the grip drawn on it.
--bmx-splitter-hit-area How far either side of the divider the pointer still grabs it.
--bmx-splitter-size The divider's thickness. Also its measured gutter, so a change here changes the arithmetic.