Tabs
<oge-tab-panel> renders a tab strip with content panels; <oge-tabs> is the strip alone. Both accept declarative <oge-tab> children and a data-driven items array, follow the WAI-ARIA tabs pattern (roving tabindex, arrow keys, automatic or manual activation) and speak RTL out of the box.
Declarative tabs
Projected <oge-tab> children carry their content. selectedIndex is a two-way model; a user gesture first fires the cancelable selectionChanging, then selectionChanged. Disabled tabs are skipped by clicks and arrow keys.
Project overview — selected index: 0
import { ChangeDetectionStrategy, Component, signal } from '@angular/core';
import { OgeTab, OgeTabPanel } from '@oge-ui/tabs';
import type { OgeTabSelectionChangedEvent } from '@oge-ui/tabs';
@Component({
selector: 'demo-root',
imports: [OgeTab, OgeTabPanel],
changeDetection: ChangeDetectionStrategy.OnPush,
template: `
<oge-tab-panel [(selectedIndex)]="index" (selectionChanged)="onChanged($event)">
<oge-tab text="Overview">Project overview…</oge-tab>
<oge-tab text="Activity">Latest activity…</oge-tab>
<oge-tab text="Settings" [disabled]="true">Settings…</oge-tab>
</oge-tab-panel>
`,
})
export class Demo {
protected readonly index = signal(0);
protected onChanged(event: OgeTabSelectionChangedEvent): void {
console.log('now on', event.index);
}
}Data-driven items
The items array drives the strip; selectedKey selects by identity, so it survives reordering and insertions. badge renders a counter, dirty the unsaved-changes dot (announced to screen readers). A component-level ogeTabContentTemplate renders every item's panel.
Editing README.md — selectedKey: readme
import { ChangeDetectionStrategy, Component, signal } from '@angular/core';
import { OgeTabContentTemplate, OgeTabPanel } from '@oge-ui/tabs';
import type { OgeTabItem } from '@oge-ui/tabs';
@Component({
selector: 'demo-root',
imports: [OgeTabContentTemplate, OgeTabPanel],
changeDetection: ChangeDetectionStrategy.OnPush,
template: `
<oge-tab-panel [items]="docs" [(selectedKey)]="active">
<ng-template ogeTabContentTemplate let-item>
Editing <b>{{ item?.text }}</b>…
</ng-template>
</oge-tab-panel>
`,
})
export class Demo {
protected readonly docs: OgeTabItem[] = [
{ key: 'readme', text: 'README.md' },
{ key: 'spec', text: 'spec.ts', badge: 3 },
{ key: 'draft', text: 'draft.md', dirty: true },
];
protected readonly active = signal('readme');
}Lazy rendering & keep-alive
With deferRendering (default) a lazy ogeTabContentTemplate is instantiated on first visit; keepAlive (default) then keeps it mounted while hidden — note the creation time does not change when you come back. Toggle keep-alive off and the content is recreated on every visit.
import { ChangeDetectionStrategy, Component, signal } from '@angular/core';
import { OgeTab, OgeTabContentTemplate, OgeTabPanel } from '@oge-ui/tabs';
@Component({
selector: 'demo-root',
imports: [OgeTab, OgeTabContentTemplate, OgeTabPanel],
changeDetection: ChangeDetectionStrategy.OnPush,
template: `
<!-- deferRendering (default true): a panel is created on first visit.
keepAlive (default true): it then stays mounted while hidden. -->
<oge-tab-panel [keepAlive]="keepAlive()">
<oge-tab text="First">
<ng-template ogeTabContentTemplate>Created at {{ stamp() }}</ng-template>
</oge-tab>
<oge-tab text="Second">
<ng-template ogeTabContentTemplate>Created at {{ stamp() }}</ng-template>
</oge-tab>
</oge-tab-panel>
`,
})
export class Demo {
protected readonly keepAlive = signal(true);
protected stamp(): string {
return new Date().toLocaleTimeString();
}
}Closable tabs & async close guard
Closing runs a pipeline: cancelable tabClosing → the tab's async closeGuard (the ✕ shows a pending spinner, extra clicks are ignored) → tabClosed, where the app removes the tab — focus hands off per the APG (following tab, else preceding). The guarded tab here asks for a second click within 3 seconds. Delete/Backspace on a focused tab closes it too.
a.ts content…
import { ChangeDetectionStrategy, Component, signal } from '@angular/core';
import { OgeTabPanel } from '@oge-ui/tabs';
import type { OgeTabClosedEvent, OgeTabItem } from '@oge-ui/tabs';
@Component({
selector: 'demo-root',
imports: [OgeTabPanel],
changeDetection: ChangeDetectionStrategy.OnPush,
template: `
<oge-tab-panel [items]="files()" [closable]="true" (tabClosed)="remove($event)" />
`,
})
export class Demo {
// the tab is removed by the app, after the guard allowed it
protected readonly files = signal<OgeTabItem[]>([
{ key: 'a.ts', text: 'a.ts' },
// async closeGuard: resolve(false) keeps the tab, rejection = veto
{
key: 'b.ts',
text: 'b.ts (guarded)',
dirty: true,
closeGuard: () => this.confirmDiscard(),
},
]);
protected remove(e: OgeTabClosedEvent): void {
this.files.set(this.files().filter((f) => f.key !== e.key));
}
private confirmDiscard(): boolean {
return confirm('Discard unsaved changes?');
}
}Overflow: arrows & all-tabs menu
When the strip overflows, showNavButtons: 'auto' reveals scroll arrows (RTL-aware) and the selected tab is kept in view. showTabListButton adds an all-tabs menu — the active tab is checked, disabled tabs stay disabled.
selected: Chapter 1
import { ChangeDetectionStrategy, Component, signal } from '@angular/core';
import { OgeTabs } from '@oge-ui/tabs';
import type { OgeTabItem } from '@oge-ui/tabs';
@Component({
selector: 'demo-root',
imports: [OgeTabs],
changeDetection: ChangeDetectionStrategy.OnPush,
template: `
<oge-tabs
[items]="manyTabs"
[(selectedIndex)]="index"
showNavButtons="auto"
[showTabListButton]="true"
/>
`,
})
export class Demo {
protected readonly manyTabs: OgeTabItem[] = Array.from(
{ length: 20 },
(_, i) => ({ key: `t${i}`, text: `Section ${i + 1}` }),
);
protected readonly index = signal(0);
}Drag reorder
Drag a header to reorder — a drop indicator marks the target, Escape cancels the drag, and the selection follows the moved tab. tabReordering is cancelable; tabReordered reports the committed move. Give tabs a key for stable identity.
To do stage…
import { ChangeDetectionStrategy, Component } from '@angular/core';
import { OgeTabPanel } from '@oge-ui/tabs';
import type { OgeTabItem, OgeTabReorderedEvent } from '@oge-ui/tabs';
@Component({
selector: 'demo-root',
imports: [OgeTabPanel],
changeDetection: ChangeDetectionStrategy.OnPush,
template: `
<oge-tab-panel
[items]="stages"
[allowTabReordering]="true"
(tabReordered)="log($event)"
/>
`,
})
export class Demo {
protected readonly stages: OgeTabItem[] = [
{ key: 'plan', text: 'Plan' },
{ key: 'build', text: 'Build' },
{ key: 'ship', text: 'Ship' },
];
protected log(event: OgeTabReorderedEvent): void {
console.log(event.fromIndex, '→', event.toIndex);
}
}Positions & styling
tabsPosition accepts logical top / bottom / start / end — vertical strips switch the arrow keys to Up/Down and RTL flips start/end for free. stylingMode='secondary' renders soft pills, size controls density.
General project settings…
import { ChangeDetectionStrategy, Component } from '@angular/core';
import { OgeTab, OgeTabPanel } from '@oge-ui/tabs';
@Component({
selector: 'demo-root',
imports: [OgeTab, OgeTabPanel],
changeDetection: ChangeDetectionStrategy.OnPush,
template: `
<oge-tab-panel tabsPosition="start" stylingMode="secondary" size="sm">
<oge-tab text="General">General settings…</oge-tab>
<oge-tab text="Members">Team members…</oge-tab>
</oge-tab-panel>
`,
})
export class Demo {}Alignment, indicator & empty state
tabAlignment distributes the tabs while they fit — justify spreads them to the edges, stretch gives each an equal share (the reference stretchTabs / tabAlignment options). indicatorFit='content' shrinks the selected-tab underline to the label (the reference fitInkBarToContent). With no visible tabs the strip renders messages.noData.
Empty strip:
import { ChangeDetectionStrategy, Component, signal } from '@angular/core';
import { OgeTabs } from '@oge-ui/tabs';
import type { OgeTabItem } from '@oge-ui/tabs';
@Component({
selector: 'demo-root',
imports: [OgeTabs],
changeDetection: ChangeDetectionStrategy.OnPush,
template: `
<!-- start (default) · center · end · justify · stretch -->
<oge-tabs
[items]="tabs"
tabAlignment="stretch"
indicatorFit="content"
[(selectedIndex)]="index"
/>
<!-- with no visible tabs the strip renders messages.noData -->
<oge-tabs [items]="[]" />
`,
})
export class Demo {
protected readonly tabs: OgeTabItem[] = [
{ key: 'one', text: 'One' },
{ key: 'two', text: 'Two' },
];
protected readonly index = signal(0);
}Panel transitions
panelAnimation fades or slides the incoming panel — slide enters from the direction of travel and mirrors itself under RTL. dynamicHeight animates the content box between panel heights instead of letting the page jump, tracking async content with a ResizeObserver. Duration is the --oge-tab-panel-transition variable rather than an input, and both are suppressed under prefers-reduced-motion.
import { ChangeDetectionStrategy, Component, signal } from '@angular/core';
import { OgeTab, OgeTabPanel } from '@oge-ui/tabs';
@Component({
selector: 'demo-root',
imports: [OgeTab, OgeTabPanel],
changeDetection: ChangeDetectionStrategy.OnPush,
template: `
<oge-tab-panel
panelAnimation="slide"
[dynamicHeight]="true"
[(selectedIndex)]="index"
>
<oge-tab text="Short">One line.</oge-tab>
<oge-tab text="Tall">Several lines of taller content…</oge-tab>
</oge-tab-panel>
`,
})
export class Demo {
protected readonly index = signal(0);
}
/* duration is a CSS variable, not an input:
.oge-tab-panel-content { --oge-tab-panel-transition: 240ms; } */