Drawer
A panel attached to one edge of its content, in one of three layout modes — and one component, not the container/drawer/content trio the reference libraries need.
Modality is derived from mode, never configured separately. overlay and push cover or displace the content, so they are dialogs: role="dialog", aria-modal, a focus trap, Escape and inert on the background. side is part of the layout, so it is a persistent landmark with none of those. WAI-ARIA has no drawer pattern and conditions modality on background interaction actually being blocked — an independent flag is exactly what lets a panel claim role="complementary" and aria-modal="true" at once.
Layout modes
overlay floats over the content, push shifts it aside without resizing it, side shrinks it so both share the row. DevExtreme calls the last one shrink and Kendo calls it push; only DevExtreme and this drawer offer all three.
overlay covers this, push shifts it, side shrinks it.
import { ChangeDetectionStrategy, Component, signal } from '@angular/core';
import { OgeDrawer } from '@oge-ui/navigation';
import type { OgeDrawerMode } from '@oge-ui/navigation';
@Component({
selector: 'demo-root',
imports: [OgeDrawer],
changeDetection: ChangeDetectionStrategy.OnPush,
template: `
<!-- One component, not the container/drawer/content trio the
reference libraries need. The panel is the [ogeDrawerPanel] slot;
everything else projected is the content. -->
<oge-drawer [(opened)]="opened" [mode]="mode()" [size]="240">
<div ogeDrawerPanel>Navigation…</div>
<main>Content that overlay covers, push shifts and side shrinks.</main>
</oge-drawer>
`,
})
export class Demo {
protected readonly opened = signal(true);
protected readonly mode = signal<OgeDrawerMode>('side');
}Position
Logical edges: start and end mirror in RTL on their own, because there is no rtlEnabled flag anywhere in this suite. Kendo is horizontal-only; this is the union of every edge the references offer.
import { ChangeDetectionStrategy, Component, signal } from '@angular/core';
import { OgeDrawer } from '@oge-ui/navigation';
import type { OgeDrawerPosition } from '@oge-ui/navigation';
@Component({
selector: 'demo-root',
imports: [OgeDrawer],
changeDetection: ChangeDetectionStrategy.OnPush,
template: `
<!-- start/end are logical: they mirror in RTL on their own,
because there is no rtlEnabled flag anywhere in this suite. -->
<oge-drawer [(opened)]="opened" mode="overlay" [position]="position()">
<div ogeDrawerPanel>Panel</div>
<main>Content</main>
</oge-drawer>
`,
})
export class Demo {
protected readonly opened = signal(false);
protected readonly position = signal<OgeDrawerPosition>('start');
}Modal drawer
An overlay drawer takes focus, traps Tab, closes on Escape and on a backdrop click, and marks the page behind it inert — which none of the four reference drawers does. Escape only acts on the topmost overlay, so a popup opened inside the drawer closes first.
Escape, or a click on the backdrop, closes it and returns focus here.
import { ChangeDetectionStrategy, Component, signal } from '@angular/core';
import { OgeDrawer } from '@oge-ui/navigation';
@Component({
selector: 'demo-root',
imports: [OgeDrawer],
changeDetection: ChangeDetectionStrategy.OnPush,
template: `
<!-- Modality is DERIVED from mode, never configured. overlay and
push cover or displace the content, so they are dialogs: role="dialog",
aria-modal, a focus trap, Escape and inert on the background. side is
part of the layout, so it is a landmark with none of those.
An independent "modal" flag is exactly what lets a panel claim
role="complementary" and aria-modal="true" at the same time. -->
<button type="button" [attr.aria-expanded]="opened()" (click)="opened.set(true)">
Menu
</button>
<oge-drawer [(opened)]="opened" mode="overlay" ariaLabel="Main menu">
<div ogeDrawerPanel>
<a href="#reports">Reports</a>
</div>
<main>Content</main>
</oge-drawer>
`,
})
export class Demo {
protected readonly opened = signal(false);
}Compact rail
minSize is the closed size — the rail that keeps icons reachable. It applies to mode="side" only: a rail belongs to the layout, and a modal drawer still partly on screen is not closed. Kendo spells this mini + miniWidth; one input covers both.
Closed it is a rail, not a gap — and it stays keyboard reachable.
import { ChangeDetectionStrategy, Component, signal } from '@angular/core';
import { OgeDrawer } from '@oge-ui/navigation';
@Component({
selector: 'demo-root',
imports: [OgeDrawer],
changeDetection: ChangeDetectionStrategy.OnPush,
template: `
<!-- minSize is the closed size: the compact rail that keeps icons
visible. It only applies to mode="side", because a rail belongs to the
layout and a modal drawer still partly on screen is not closed. -->
<oge-drawer [(opened)]="opened" mode="side" [size]="240" [minSize]="56">
<div ogeDrawerPanel>Icons, then labels once open</div>
<main>Content</main>
</oge-drawer>
`,
})
export class Demo {
protected readonly opened = signal(false);
}Responsive downgrade
DevExtreme and Kendo watch the window. This one measures its own container, so a drawer nested in a dialog, a split pane or this card adapts to the room it actually has. The decision is core's pure resolveDrawerMode(), unit-tested without a DOM.
resolved mode → side
import { ChangeDetectionStrategy, Component, signal } from '@angular/core';
import { OgeDrawer } from '@oge-ui/navigation';
import type { OgeDrawerModeChangedEvent } from '@oge-ui/navigation';
@Component({
selector: 'demo-root',
imports: [OgeDrawer],
changeDetection: ChangeDetectionStrategy.OnPush,
template: `
<!-- compactBelow measures the drawer's OWN container, never the
window, so a drawer nested in a dialog or a split pane adapts to the room
it actually has. Below the threshold it downgrades to an overlay and
closes, rather than leaving a backdrop the user never asked for. -->
<oge-drawer
[(opened)]="opened"
mode="side"
[compactBelow]="720"
(modeChanged)="onMode($event)"
>
<div ogeDrawerPanel>Navigation</div>
<main>Content</main>
</oge-drawer>
`,
})
export class Demo {
protected readonly opened = signal(true);
protected onMode(event: OgeDrawerModeChangedEvent): void {
console.log(event.mode, 'compact:', event.compact);
}
}Close guard
The overlay package's veto semantics, reused verbatim: false, a throw and a rejection all mean “stay open”, a promise reports pending, and a second close gesture meanwhile is dropped.
import { ChangeDetectionStrategy, Component, signal } from '@angular/core';
import { OgeDrawer } from '@oge-ui/navigation';
@Component({
selector: 'demo-root',
imports: [OgeDrawer],
changeDetection: ChangeDetectionStrategy.OnPush,
template: `
<!-- closeGuard follows the overlay package's veto semantics:
false, a throw and a rejection all mean "stay open", a promise reports
pending through closePending, and a second gesture meanwhile is dropped. -->
<oge-drawer [(opened)]="opened" [closeGuard]="confirmDiscard">
<form ogeDrawerPanel>Unsaved edits…</form>
<main>Content</main>
</oge-drawer>
`,
})
export class Demo {
protected readonly opened = signal(true);
protected readonly dirty = signal(true);
protected readonly confirmDiscard = (): boolean =>
!this.dirty() || confirm('Discard your changes?');
}App shell
The whole shell out of OGE containers: a toolbar on top, a drawer down the side holding the tree view that ships in the same package, and a splitter dividing the workspace. Drag the width and the shell reorganises itself from its own size.
import { ChangeDetectionStrategy, Component, signal } from '@angular/core';
import { OgeDrawer, OgeTreeView } from '@oge-ui/navigation';
import { OgeToolbar, OgeToolbarItem, OgeSplitter, OgeSplitterPane } from '@oge-ui/layout';
@Component({
selector: 'demo-root',
imports: [OgeDrawer, OgeTreeView, OgeToolbar, OgeToolbarItem, OgeSplitter, OgeSplitterPane],
changeDetection: ChangeDetectionStrategy.OnPush,
template: `
<!-- The whole app shell out of three OGE containers: a toolbar on
top, a drawer down the side holding the tree view that ships in the same
package, and a splitter dividing the workspace.
compactBelow makes the shell responsive to its own width, so the same
markup works full-page and inside a preview card. -->
<oge-toolbar ariaLabel="Application">
<oge-toolbar-item text="Menu" (itemClick)="menuOpen.set(!menuOpen())" />
<oge-toolbar-item text="Save" severity="accent" [overflowPriority]="10" />
<oge-toolbar-item text="Help" location="after" [overflowPriority]="-1" />
</oge-toolbar>
<oge-drawer
[(opened)]="menuOpen"
mode="side"
[size]="220"
[compactBelow]="720"
ariaLabel="Sections"
>
<oge-tree-view ogeDrawerPanel [items]="nav" />
<oge-splitter [(sizes)]="sizes">
<oge-splitter-pane key="list">Rows…</oge-splitter-pane>
<oge-splitter-pane key="detail">Details…</oge-splitter-pane>
</oge-splitter>
</oge-drawer>
`,
})
export class Demo {
protected readonly menuOpen = signal(true);
protected readonly sizes = signal<readonly number[]>([60, 40]);
protected readonly nav = [
{ id: 1, parentId: null, text: 'Reports' },
{ id: 2, parentId: 1, text: 'Monthly' },
{ id: 3, parentId: null, text: 'Settings' },
];
}Configuration
Every user-facing string, including the panel's accessible name, lives in the messages interface — overridable application-wide or per instance with [messages].
import { provideOgeDrawerConfig } from '@oge-ui/navigation';
bootstrapApplication(App, {
providers: [
provideOgeDrawerConfig({
mode: 'side',
size: 280,
messages: { drawer: 'Gezinme', close: 'Kapat' },
}),
],
});