Splitter
<oge-splitter> divides an area into resizable panes that come from projected <oge-splitter-pane> children, from a data-driven panes array, or both. Sizes are ratios, not percentages, so a configuration that does not add up to 100 is never an error — and a '240px' size pins a pane instead.
Layout is a single CSS grid in which the separators are real tracks, so panes mirror automatically in RTL and no pane needs an inline width.
Resizable panes
Drag the separator, or Tab to it and use the arrow keys. sizes is a two-way model reporting the current ratios — the only state you need to keep.
sizes → [35, 65]
import { ChangeDetectionStrategy, Component, signal } from '@angular/core';
import { OgeSplitter, OgeSplitterPane } from '@oge-ui/layout';
@Component({
selector: 'demo-root',
imports: [OgeSplitter, OgeSplitterPane],
changeDetection: ChangeDetectionStrategy.OnPush,
template: `
<oge-splitter [(sizes)]="sizes">
<oge-splitter-pane key="list" [minSize]="15">
Result list…
</oge-splitter-pane>
<oge-splitter-pane key="detail" [minSize]="25">
Detail view…
</oge-splitter-pane>
</oge-splitter>
`,
})
export class Demo {
// Sizes are ratios, not percentages — [30, 30] lays out like [50, 50],
// so a configuration that does not add up to 100 is never an error.
protected readonly sizes = signal([35, 65]);
}Orientation
horizontal lays the panes out side by side, vertical stacks them. The keyboard follows the axis: Left/Right against a horizontal splitter, Up/Down against a vertical one.
import { ChangeDetectionStrategy, Component, signal } from '@angular/core';
import { OgeSplitter, OgeSplitterPane } from '@oge-ui/layout';
import type { OgeSplitterOrientation } from '@oge-ui/layout';
@Component({
selector: 'demo-root',
imports: [OgeSplitter, OgeSplitterPane],
changeDetection: ChangeDetectionStrategy.OnPush,
template: `
<oge-splitter [orientation]="orientation()">
<oge-splitter-pane>Top / left</oge-splitter-pane>
<oge-splitter-pane>Bottom / right</oge-splitter-pane>
</oge-splitter>
`,
})
export class Demo {
protected readonly orientation = signal<OgeSplitterOrientation>('vertical');
}Fixed and fluid panes
A '240px' size becomes a fixed grid track and leaves the share pool; dragging it moves real pixels. minSize and maxSize accept either unit, so a pixel floor on a ratio pane is fine.
import { ChangeDetectionStrategy, Component } from '@angular/core';
import { OgeSplitter, OgeSplitterPane } from '@oge-ui/layout';
@Component({
selector: 'demo-root',
imports: [OgeSplitter, OgeSplitterPane],
changeDetection: ChangeDetectionStrategy.OnPush,
template: `
<!-- A '<n>px' size pins the pane: it becomes a fixed grid track and
drops out of the share pool, while '<n>%' and plain numbers stay ratios.
min and max accept either unit, so a px floor on a ratio pane is fine. -->
<oge-splitter>
<oge-splitter-pane size="240px" minSize="160px" maxSize="420px">
Fixed sidebar — dragged in pixels
</oge-splitter-pane>
<oge-splitter-pane [minSize]="20">Fluid content</oge-splitter-pane>
</oge-splitter>
`,
})
export class Demo {}Collapsible panes
A separator grows one grip per collapsible neighbour, so either side can be collapsed. Enter targets the pane before it (the APG primary pane) and Ctrl+Arrow reaches both. The pane returns at the size it left, and while collapsed it stays in the DOM as inert so aria-controls keeps pointing at a real element.
collapsed → false
import { ChangeDetectionStrategy, Component, signal } from '@angular/core';
import { OgeSplitter, OgeSplitterPane } from '@oge-ui/layout';
import type { OgeSplitterPaneCollapsedEvent } from '@oge-ui/layout';
@Component({
selector: 'demo-root',
imports: [OgeSplitter, OgeSplitterPane],
changeDetection: ChangeDetectionStrategy.OnPush,
template: `
<!-- Enter on the separator, the grip, or a double click all collapse the
pane before it. It comes back at the size it left, and stays in the DOM
as an inert element so aria-controls keeps pointing at something real. -->
<oge-splitter (paneCollapsed)="onCollapsed($event)">
<oge-splitter-pane
key="side"
[size]="30"
[collapsible]="true"
collapsedSize="28px"
[(collapsed)]="sideCollapsed"
>
Navigation…
</oge-splitter-pane>
<oge-splitter-pane key="main">Editor…</oge-splitter-pane>
</oge-splitter>
`,
})
export class Demo {
protected readonly sideCollapsed = signal(false);
protected onCollapsed(event: OgeSplitterPaneCollapsedEvent): void {
console.log('collapsed', event.key);
}
}Data-driven panes
Bind panes and render the bodies from one template. Declarative children and panes entries can be mixed — children come first, the same merge rule as the tabs and accordion families.
import { ChangeDetectionStrategy, Component } from '@angular/core';
import { OgeSplitter, OgeSplitterPaneTemplate } from '@oge-ui/layout';
import type { OgeSplitterPaneData } from '@oge-ui/layout';
@Component({
selector: 'demo-root',
imports: [OgeSplitter, OgeSplitterPaneTemplate],
changeDetection: ChangeDetectionStrategy.OnPush,
template: `
<oge-splitter [panes]="areas">
<ng-template ogeSplitterPaneTemplate let-pane let-index="index">
<h4>{{ index }} — {{ pane.key }}</h4>
</ng-template>
</oge-splitter>
`,
})
export class Demo {
protected readonly areas: OgeSplitterPaneData[] = [
{ key: 'explorer', size: 25, minSize: 15, collapsible: true },
{ key: 'editor', size: 50 },
{ key: 'inspector', size: 25, minSize: 15 },
];
}Nested splitters
A splitter inside a pane just works — no second component and no wrapper. A data-driven pane nests by carrying its own panes, which defaults to the opposite axis.
import { ChangeDetectionStrategy, Component } from '@angular/core';
import { OgeSplitter, OgeSplitterPane } from '@oge-ui/layout';
@Component({
selector: 'demo-root',
imports: [OgeSplitter, OgeSplitterPane],
changeDetection: ChangeDetectionStrategy.OnPush,
template: `
<!-- Nesting needs no second component: a splitter inside a pane just
works, and a data-driven pane nests by carrying its own panes array
(which defaults to the opposite axis). -->
<oge-splitter>
<oge-splitter-pane size="220px" [collapsible]="true">
Sidebar
</oge-splitter-pane>
<oge-splitter-pane>
<oge-splitter orientation="vertical">
<oge-splitter-pane [size]="70">Editor</oge-splitter-pane>
<oge-splitter-pane [size]="30" [collapsible]="true">
Terminal
</oge-splitter-pane>
</oge-splitter>
</oge-splitter-pane>
</oge-splitter>
`,
})
export class Demo {}Forms inside a pane
A pane is a plain block box and never a query container, so an <oge-form> inside one keeps resolving its @container queries against itself. Drag the separator: the column count follows the pane width while the window stays put.
import { ChangeDetectionStrategy, Component, signal } from '@angular/core';
import { OgeForm } from '@oge-ui/forms';
import { OgeSplitter, OgeSplitterPane } from '@oge-ui/layout';
import type { OgeFormItemData } from '@oge-ui/forms';
@Component({
selector: 'demo-root',
imports: [OgeForm, OgeSplitter, OgeSplitterPane],
changeDetection: ChangeDetectionStrategy.OnPush,
template: `
<!-- A pane is a plain block box, never a query container, so a form
inside one keeps resolving its @container queries against itself.
Drag the separator and the column count follows the PANE width —
the window never moves. -->
<oge-splitter>
<oge-splitter-pane [size]="78">
<oge-form
[(formData)]="server"
[items]="fields"
[colCountByScreen]="{ xs: 1, sm: 2, md: 3 }"
/>
</oge-splitter-pane>
<oge-splitter-pane [size]="22">Preview…</oge-splitter-pane>
</oge-splitter>
`,
})
export class Demo {
protected readonly server = signal({ host: '', port: 5432, user: '' });
protected readonly fields: OgeFormItemData[] = [
{ field: 'host', label: 'Host' },
{ field: 'port', label: 'Port' },
{ field: 'user', label: 'User' },
];
}Keyboard & accessibility
Tab to a separator, then Arrow keys to move it by step, Home and End for the primary pane's smallest and largest size, and Enter to collapse or restore it. Ctrl+Arrow reaches either neighbour — the keyboard path to the second grip. Values are reported on one 0–100 scale via aria-valuenow.
import { ChangeDetectionStrategy, Component } from '@angular/core';
import { OgeSplitter, OgeSplitterPane } from '@oge-ui/layout';
@Component({
selector: 'demo-root',
imports: [OgeSplitter, OgeSplitterPane],
changeDetection: ChangeDetectionStrategy.OnPush,
template: `
<!-- WAI-ARIA APG window splitter: every separator is a focusable
role="separator" with aria-controls on the pane before it and
aria-valuenow/min/max on one 0-100 scale.
Arrow keys move it by [step] share points (RTL mirrored)
Home / End jump to the primary pane's smallest / largest size
Enter collapse the primary pane, or restore it
Ctrl + Arrow collapse the pane the arrow points at, or restore
the collapsed one it points away from -->
<oge-splitter [step]="10" ariaLabel="Editor layout">
<oge-splitter-pane [minSize]="20" [maxSize]="70" [collapsible]="true">
Primary
</oge-splitter-pane>
<oge-splitter-pane [minSize]="20" [collapsible]="true">
Secondary
</oge-splitter-pane>
</oge-splitter>
`,
})
export class Demo {}Events
resizeStarted fires once, resized on every change and resizeEnded once the gesture settles — the same trio the references expose. paneCollapsing and paneExpanding are cancelable.
import { ChangeDetectionStrategy, Component, signal } from '@angular/core';
import { OgeSplitter, OgeSplitterPane } from '@oge-ui/layout';
import type { OgeSplitterPaneCollapsingEvent, OgeSplitterResizeEvent } from '@oge-ui/layout';
@Component({
selector: 'demo-root',
imports: [OgeSplitter, OgeSplitterPane],
changeDetection: ChangeDetectionStrategy.OnPush,
template: `
<oge-splitter
(resizeStarted)="log('start')"
(resized)="onResized($event)"
(resizeEnded)="log('end')"
(paneCollapsing)="onCollapsing($event)"
>
<oge-splitter-pane key="a" [collapsible]="true">A</oge-splitter-pane>
<oge-splitter-pane key="b">B</oge-splitter-pane>
</oge-splitter>
`,
})
export class Demo {
protected readonly locked = signal(true);
protected onResized(event: OgeSplitterResizeEvent): void {
console.log(event.sizes, event.previousSizes);
}
// paneCollapsing / paneExpanding are cancelable — set cancel to veto.
protected onCollapsing(event: OgeSplitterPaneCollapsingEvent): void {
if (this.locked()) event.cancel = true;
}
protected log(phase: string): void {
console.log(phase);
}
}Persisting sizes
sizes is the whole persistable state — a plain array of numbers and '<n>px' strings. There is no stateKey to learn and no storage token to provide: save it to localStorage, an API or a route param in a few lines.
persist this → [30, 70]
import { ChangeDetectionStrategy, Component, effect, signal } from '@angular/core';
import { OgeSplitter, OgeSplitterPane } from '@oge-ui/layout';
@Component({
selector: 'demo-root',
imports: [OgeSplitter, OgeSplitterPane],
changeDetection: ChangeDetectionStrategy.OnPush,
template: `
<oge-splitter [(sizes)]="sizes">
<oge-splitter-pane>Left</oge-splitter-pane>
<oge-splitter-pane>Right</oge-splitter-pane>
</oge-splitter>
`,
})
export class Demo {
// [(sizes)] is the whole persistable state, so there is no stateKey to
// learn and no storage token to provide — save it wherever you like.
protected readonly sizes = signal<(number | string)[]>(
JSON.parse(localStorage.getItem('editor-layout') ?? 'null') ?? [30, 70],
);
constructor() {
effect(() =>
localStorage.setItem('editor-layout', JSON.stringify(this.sizes())),
);
}
}Configuration
provideOgeSplitterConfig() sets application-wide defaults and every user-facing string, including the separators' accessible names. A per-instance [messages] input overrides it.
import { provideOgeSplitterConfig } from '@oge-ui/layout';
export const appConfig: ApplicationConfig = {
providers: [
provideOgeSplitterConfig({
separatorSize: 8,
step: 10,
messages: {
separator: '{{first}} ile {{second}} arasını yeniden boyutlandır',
collapsePane: 'Paneli daralt',
expandPane: 'Paneli aç',
},
}),
],
};