Progress & Loading
Three canonical loading visuals — the linear oge-progress-bar, the ring oge-load-indicator and the oge-skeleton placeholder — replacing the hand-drawn spinners and shimmers every OGE surface carried before. All announce as role="progressbar" with the ARIA rule most libraries miss: indeterminate omits aria-valuenow entirely rather than pinning a sentinel.
Not a meter: the APG is explicit that a current measurement within a known range — battery, disk usage — is role="meter", and "the meter should not be used to indicate progress… use the progressbar role instead". The inverse holds too. (Where a native <progress> suffices, MDN recommends it over the role — these components exist for the styled, token-driven cases.)
Determinate bar
Transform-driven fill (no layout work per frame, mirrors in RTL), value changes glide on a token transition. formatLabel replaces the percent label and feeds aria-valuetext — display and announcement never diverge.
import { ChangeDetectionStrategy, Component, signal } from '@angular/core';
import { OgeProgressBar } from '@oge-ui/layout';
@Component({
selector: 'demo-root',
imports: [OgeProgressBar],
changeDetection: ChangeDetectionStrategy.OnPush,
template: `
<!-- role="progressbar" with the full aria triple. showLabel
renders the rounded percent; formatLabel replaces it AND feeds
aria-valuetext, so display and announcement never diverge. -->
<oge-progress-bar [value]="uploaded()" [showLabel]="true" />
<oge-progress-bar
[value]="uploaded()"
[max]="200"
[showLabel]="true"
[formatLabel]="asMegabytes"
ariaLabel="Upload"
/>
`,
})
export class Demo {
protected readonly uploaded = signal(80);
protected readonly asMegabytes = (value: number): string => `${value} MB`;
}Indeterminate & buffer
value: null (the default) renders the sliding bar and omits aria-valuenow — the ARIA rule for the unknown state. bufferValue adds the second layer Material uses for media pre-loading.
import { ChangeDetectionStrategy, Component, signal } from '@angular/core';
import { OgeProgressBar } from '@oge-ui/layout';
@Component({
selector: 'demo-root',
imports: [OgeProgressBar],
changeDetection: ChangeDetectionStrategy.OnPush,
template: `
<!-- value: null (the default) is the indeterminate sliding bar.
Per the ARIA guidance aria-valuenow is then OMITTED entirely — never
pinned to a sentinel. bufferValue adds Material's second layer (media
pre-loading behind the play position). -->
<oge-progress-bar ariaLabel="Preparing" />
<oge-progress-bar [value]="played()" [bufferValue]="buffered()" ariaLabel="Playback" />
`,
})
export class Demo {
protected readonly played = signal(35);
protected readonly buffered = signal(70);
}Chunks & severity
chunkCount renders the segmented variant for step-based flows; severity recolors the fill with the card/toast vocabulary.
import { ChangeDetectionStrategy, Component, signal } from '@angular/core';
import { OgeProgressBar } from '@oge-ui/layout';
@Component({
selector: 'demo-root',
imports: [OgeProgressBar],
changeDetection: ChangeDetectionStrategy.OnPush,
template: `
<!-- chunkCount renders Kendo's segmented variant — the filled
segment count is the rounded ratio. severity recolors the fill with the
card/toast vocabulary. -->
<oge-progress-bar [value]="step() * 20" [chunkCount]="5" ariaLabel="Steps" />
<oge-progress-bar [value]="92" severity="success" [showLabel]="true" ariaLabel="Sync" />
<oge-progress-bar [value]="45" severity="danger" [showLabel]="true" ariaLabel="Disk" />
`,
})
export class Demo {
protected readonly step = signal(3);
}Load indicator
The suite's canonical ring — deliberately indeterminate-only: a circle filling toward completion is the progress bar's job. Under prefers-reduced-motion the spin slows rather than stops, because a frozen ring reads as finished.
import { ChangeDetectionStrategy, Component } from '@angular/core';
import { OgeLoadIndicator } from '@oge-ui/layout';
@Component({
selector: 'demo-root',
imports: [OgeLoadIndicator],
changeDetection: ChangeDetectionStrategy.OnPush,
template: `
<!-- The suite's canonical ring, deliberately indeterminate-only
(a circle filling toward completion is the progress bar's job). Under
prefers-reduced-motion it slows rather than stops — a frozen ring reads
as finished. inheritSize makes a 1em ring for buttons. -->
<oge-load-indicator size="sm" />
<oge-load-indicator />
<oge-load-indicator size="lg" ariaLabel="Loading report" />
<button type="button" disabled>
<oge-load-indicator [inheritSize]="true" /> Saving…
</button>
`,
})
export class Demo {}Skeleton
Always aria-hidden decoration — the loading region owns the announcement: put aria-busy (plus a visually-hidden status text where the change should be announced) on the container. shimmer is the card/accordion gradient; pulse is the data grid's beat.
import { ChangeDetectionStrategy, Component, signal } from '@angular/core';
import { OgeSkeleton } from '@oge-ui/layout';
@Component({
selector: 'demo-root',
imports: [OgeSkeleton],
changeDetection: ChangeDetectionStrategy.OnPush,
template: `
<!-- Always aria-hidden decoration: the loading REGION owns the
announcement — put aria-busy (and a visually-hidden status text where
the change should be announced) on the container. shimmer is the
card/accordion gradient recipe; pulse is the grid filler rows' beat. -->
<div [attr.aria-busy]="loading() ? true : null" class="flex items-center gap-3">
<oge-skeleton shape="circle" [width]="40" [height]="40" />
<div class="flex-1">
<oge-skeleton [width]="'60%'" />
<oge-skeleton class="mt-2" [width]="'40%'" animation="pulse" />
</div>
</div>
<oge-skeleton shape="rectangle" height="96px" class="mt-3" />
<!-- lines: the tapered multi-line text stack in one input. -->
<oge-skeleton [lines]="3" class="mt-3" />
`,
})
export class Demo {
protected readonly loading = signal(true);
}A real async flow
Indeterminate while the total is unknown, determinate once it is, and a one-shot completed at the end — fired once per arrival at max, again only after a reset.
import { ChangeDetectionStrategy, Component, signal } from '@angular/core';
import { OgeProgressBar, OgeSkeleton } from '@oge-ui/layout';
@Component({
selector: 'demo-root',
imports: [OgeProgressBar, OgeSkeleton],
changeDetection: ChangeDetectionStrategy.OnPush,
template: `
<!-- A real flow: indeterminate while the size is unknown, then
determinate, then a one-shot completed event. -->
@if (total() === null) {
<oge-progress-bar ariaLabel="Download" />
} @else {
<oge-progress-bar
[value]="received()"
[max]="total()!"
[showLabel]="true"
ariaLabel="Download"
(completed)="done.set(true)"
/>
}
`,
})
export class Demo {
protected readonly total = signal<number | null>(null);
protected readonly received = signal(0);
protected readonly done = signal(false);
}