<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.
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.
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.
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
| Property | Attribute | Type | Default | Description |
|---|---|---|---|---|
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
| Event | Detail | Description |
|---|---|---|
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
| Method | Signature | Description |
|---|---|---|
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
| Slot | Description |
|---|---|
end |
The second pane. |
start |
The first pane: the left one, or the top one, and the right one under RTL. |
CSS shadow parts
| Part | Description |
|---|---|
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
| Property | Description |
|---|---|
--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. |