GrSplitter
Splits a layout into two resizable panels with a draggable divider.
Overview
When to take it
- two areas need a shared boundary — a tree and the content, an editor and a preview, a list and a detail;
- the size is chosen by the user — the boundary moves with the mouse and from the keyboard, and
min/maxhold the edges; - one area is collapsed —
collapsibleremoves a panel entirely and brings it back; - the layout is saved —
v-modelgives away the size, and all that is left is to write it down.
When to take something else
| Need | Take |
|---|---|
| There are many areas and they are on a grid | GrDashboard |
| The layout is fixed | CSS Grid and GrCard |
| The panel slides out over the content | GrDrawer |
| Sections are switched | GrTabs |
| Just a line between blocks | GrDivider |
Playground controls come from the API, not from hand
Loading…
0 changed· state is in the URL
<GrSplitter>Label</GrSplitter>Examples 2 each with a live demo and its code
A conversation list and the open message
The model is the share of the list in per cent: it survives a narrower window, and the server renders the same split, so hydration is clean.
minandmaxkeep the list readable,min-endkeeps the message from being squeezed to nothing.changecomes at the end of a drag and on every arrow step — the place to save the layout; a double click on the border returns todefault-size.Open · 4Layout: list 38%#4182 · Northwind TradersCard declined on renewal Our card was declined this morning, but the bank says. Can you check what happened on your side? We would like to keep the subscription active.
Tables, a query and its result
Three panes are two splitters, one inside the other, each with its own model. The table list folds away with
collapsibleandv-model:collapsed— the share survives the fold, so the pane returns where it was; Enter on the border and a drag against the edge fold it too.orientation="vertical"divides the editor from the result,stepandbig-stepset the arrows, and a locked layout isdisabled.- billing · 6 tables
- customers
- invoices
- invoice_lines
- payments
- refunds
- subscriptions
SELECT c.name, s.plan, SUM(i.total) AS billed FROM customers c JOIN subscriptions s ON s.customer_id = c.id JOIN invoices i ON i.customer_id = c.id WHERE i.issued_at >= DATE '2026-09-01' GROUP BY c.name, s.plan ORDER BY billed DESC;
3 rows · 41 msNorthwind Traders Business €4,820.00 Contoso Ltd Team €1,296.00 Fabrikam GmbH Team €1,104.00
API generated from the package · cannot drift
Props
| Prop | Type | default | Description |
|---|---|---|---|
disabled | boolean | undefined | false | The separator does not drag, does not take focus and does not answer to keys. |
modelValuev-model | number | undefined | undefined | The share of the first panel as a percentage. It supports `v-model`; without it the component remembers the size itself. |
ariaLabel | string | undefined | undefined | The name of the separator for a screen reader. Unset — from the locale. |
max | number | undefined | 90 | The maximum of the first panel as a percentage. |
orientation | "horizontal" | "vertical" | undefined | undefined | `horizontal` — the panels side by side, `vertical` — one above the other. |
step | number | undefined | 1 | The step of the arrows. |
min | number | undefined | 10 | The minimum of the first panel as a percentage. |
collapsedv-model | boolean | undefined | false | The first panel is collapsed. It supports `v-model:collapsed`. |
defaultSize | number | undefined | 50 | The starting share and the point to return to on a double click. |
minEnd | number | undefined | 10 | The minimum of the second panel: without it the panel can be squeezed to zero. |
bigStep | number | undefined | 10 | The step of the arrows with `Shift`. |
collapsible | boolean | undefined | undefined | Permit collapsing the first panel. |
Slots
| Slot | Type | Description |
|---|---|---|
start | any | The first panel — the one whose size is set by the model. |
end | any | The second panel: it takes the remainder. |
Events
| Event | Signature | Description |
|---|---|---|
update:modelValue | (event: "update:modelValue", value: number): void | — |
change | (event: "change", value: number): void | — |
update:collapsed | (event: "update:collapsed", value: boolean): void | — |
Types
Every exported type of the subpath, grouped by the file it lives in. Names, generics and bodies come from the package’s declaration files and cannot drift.
import type { GrSplitterProps, GrSplitterEmits, GrSplitterOrientation } from '@feugene/granularity/components/GrSplitter'Filedist/types/components/GrSplitter/GrSplitter.vue.d.ts
interface GrSplitterProps {
modelValue?: number;
defaultSize?: number;
orientation?: GrSplitterOrientation;
min?: number;
max?: number;
minEnd?: number;
step?: number;
bigStep?: number;
collapsible?: boolean;
collapsed?: boolean;
disabled?: boolean;
ariaLabel?: string;
}interface GrSplitterEmits {
(e: 'update:modelValue', value: number): void;
(e: 'update:collapsed', value: boolean): void;
(e: 'change', value: number): void;
}Filedist/types/components/GrSplitter/splitterGeometry.d.ts
type GrSplitterOrientation = typeof GR_SPLITTER_ORIENTATIONS[number];interface GrSplitterBounds {
min: number;
max: number;
minEnd: number;
}function sizeFromPointer(rect: {
left: number;
top: number;
width: number;
height: number;
}, clientX: number, clientY: number, orientation: GrSplitterOrientation): number;Filedist/types/components/GrSplitter/config.d.ts
const grSplitterConfig: import("@feugene/granum/contract").GranumComponentDescriptor<"GrSplitter">;Filedist/types/components/GrSplitter/safelist.d.ts
const grSplitterSafelist: string[];Filedist/types/components/GrSplitter/defaults.d.ts
interface GrSplitterConfigurableProps {
orientation: GrSplitterOrientation;
collapsible: boolean;
}Filedist/types/components/GrSplitter/index.d.ts
type GrSplitterInstance = ComponentExposed<typeof GrSplitterComponent>;Accessibility
APG pattern
window splitter
Keys
Home / EndEnter
Full contract of the package: docs/keyboard.md ↗
Usage from the library prose
Three panels means nesting
The component holds exactly two panels and one divider. The “tree | editor / console” layout is assembled by nesting, as in code editors:
<GrSplitter v-model="treeWidth">
<template #start><FileTree /></template>
<template #end>
<GrSplitter v-model="consoleHeight" orientation="vertical">
<template #start><Editor /></template>
<template #end><Console /></template>
</GrSplitter>
</template>
</GrSplitter>Nested splitters are independent: each runs a model of its own.
The bounds of the panels
min and max limit the first panel, and minEnd the second. Without minEnd the second panel
can be squeezed to zero, and there will be nothing to bring it back with.
In a conflict between min and minEnd the min wins: the first panel does not go below its
minimum even if the second one turns out narrower than its own. The reverse order would give a
panel the user cannot get out of.
The keyboard and the mouse
The divider stands in the tab order (role="separator", tabindex="0"):
←/→in the horizontal layout,↑/↓in the vertical one — a step;Shift+ an arrow — a large step;Home/End— tomin/max;Enter— collapse and bring back (withcollapsible);- a double click — a reset to
defaultSize; - dragging right up to the edge (below half of
min) collapses the panel with the mouse: the double click is taken by the reset, and there has to be a way to collapse with the mouse.
The grab zone is wider than the visible strip: a six-pixel target is not caught with a mouse, and making the strip thicker for that would mean painting the layout to suit the mouse.
aria-orientation is inverted — and that is right
orientation describes the layout: horizontal means the panels stand side by side.
aria-orientation describes the divider itself, and panels standing side by side are separated
by a vertical strip. A horizontal layout therefore has aria-orientation="vertical", and vice
versa.
The divider announces aria-valuenow/valuemin/valuemax in the same percentages as the model
and refers to the first panel through aria-controls.
Being collapsed is not a zero in the model
collapsed is a separate state (v-model:collapsed) rather than modelValue: 0. The previous
size has to survive the collapsing, otherwise Enter would bring the panel back to the wrong
place. In the track a collapsed panel takes up 0 %, and aria-valuenow shows 0 — what is
visible on the screen.
A collapsed panel is clipped and hidden (overflow: hidden, visibility: hidden) and marked
inert: its content is not painted over the second panel, is not announced by a screen reader and
does not get into the Tab order. Previously a zero width left the content visible over the
neighbour, and the links inside it reachable blindly. On expanding, everything comes back.
Limits
GrResizable(resizing a block by a corner) does not belong here — a different scenario and a different pattern;- saving the layout is the job of the application:
v-modelandchangegive everything necessary, and alocalStorageinside the component would be state the application knows nothing about; - there are no snap points and no “magnets” — without a scenario that is extra mechanics on top of the clamping;
- there is no touch physics of its own — the
pointerevents cover touch as it is.
Props in this example