FAB & Speed Dial
oge-fab is the screen's one primary action, pinned to a viewport corner or edge with the safe-area insets as a floor; oge-speed-dial unfolds a short list of related actions from it, following the WAI-ARIA APG menu button pattern: the FAB is a aria-haspopup="menu" button, the actions are menu items, opening moves focus to the nearest one.
The demos pin their FABs inside a box (positionMode="absolute"); in an app the default fixed pins them to the viewport.
Floating action button
An icon-only FAB takes its accessible name from label. Pass the icon as SVG path data, or project your own aria-hidden icon. clicked reports the press.
Pressed 0 times
import { ChangeDetectionStrategy, Component } from '@angular/core';
import { OgeFab } from '@oge-ui/buttons';
const PENCIL = 'M4 20h4L19 9l-4-4L4 16v4Z';
@Component({
selector: 'demo-root',
imports: [OgeFab],
changeDetection: ChangeDetectionStrategy.OnPush,
template: `
<!-- An icon-only FAB is named by its label. positionMode="absolute"
pins it inside the nearest positioned ancestor; the default "fixed"
pins it to the viewport, with safe-area insets as a floor. -->
<div style="position: relative; height: 224px">
<oge-fab
label="Compose"
[icon]="pencil"
positionMode="absolute"
(clicked)="compose()"
/>
</div>
`,
})
export class Demo {
protected readonly pencil = PENCIL;
protected compose(): void {
console.log('compose');
}
}Extended FAB
extended shows the label as text beside the icon in a pill. Sizes are 40 / 56 / 72 px; severity uses the button vocabulary (normal is the page surface).
import { ChangeDetectionStrategy, Component } from '@angular/core';
import { OgeFab } from '@oge-ui/buttons';
const PENCIL = 'M4 20h4L19 9l-4-4L4 16v4Z';
const SHARE = 'M4 12v7h16v-7M12 3v12M7 8l5-5 5 5';
@Component({
selector: 'demo-root',
imports: [OgeFab],
changeDetection: ChangeDetectionStrategy.OnPush,
template: `
<!-- extended shows the label as text beside the icon (a pill);
size and severity follow the button vocabulary. -->
<oge-fab label="Compose" [icon]="pencil" [extended]="true" positionMode="static" />
<oge-fab label="Share" [icon]="share" size="sm" severity="normal" positionMode="static" />
<oge-fab label="Share" [icon]="share" severity="success" positionMode="static" />
<oge-fab label="Share" [icon]="share" size="lg" severity="danger" positionMode="static" />
`,
})
export class Demo {
protected readonly pencil = PENCIL;
protected readonly share = SHARE;
}Positions & safe areas
Six logical positions — start/end mirror in RTL. Pinned edges use max(offset, env(safe-area-inset-*)), so a fixed FAB clears the home indicator and the notch once the app opts into viewport-fit=cover, and keeps the plain gap everywhere else.
import { ChangeDetectionStrategy, Component } from '@angular/core';
import { OgeFab } from '@oge-ui/buttons';
import type { OgeFabPosition } from '@oge-ui/buttons';
const PENCIL = 'M4 20h4L19 9l-4-4L4 16v4Z';
@Component({
selector: 'demo-root',
imports: [OgeFab],
changeDetection: ChangeDetectionStrategy.OnPush,
template: `
<!-- Six logical positions (start/end mirror in RTL). Pinned edges
use max(offset, env(safe-area-inset-*)), so the FAB clears a phone's
home indicator once the app opts into viewport-fit=cover. -->
<div style="position: relative; height: 256px">
@for (position of positions; track position) {
<oge-fab
[label]="position"
[icon]="pencil"
size="sm"
positionMode="absolute"
[position]="position"
offset="12px"
/>
}
</div>
`,
})
export class Demo {
protected readonly pencil = PENCIL;
protected readonly positions: OgeFabPosition[] = [
'top-start',
'top-center',
'top-end',
'bottom-start',
'bottom-center',
'bottom-end',
];
}Speed dial
Enter, Space, a click or the arrow pointing along the dial opens it with focus on the nearest action (the arrow back opens it on the last). The arrows move and wrap, Home/End jump, Escape closes and returns focus to the FAB, Tab closes and moves on, a press outside closes it.
Last action: none · opened: false
import { ChangeDetectionStrategy, Component, signal } from '@angular/core';
import { OgeSpeedDial } from '@oge-ui/buttons';
import type { OgeSpeedDialItem, OgeSpeedDialItemClickEvent } from '@oge-ui/buttons';
const MAIL = 'M3 6h18v12H3zM3 6l9 7 9-7';
const PRINT = 'M6 9V3h12v6M6 18H4v-7h16v7h-2M8 14h8v7H8z';
const SHARE = 'M4 12v7h16v-7M12 3v12M7 8l5-5 5 5';
const TRASH = 'M4 7h16M10 11v6M14 11v6M6 7l1 14h10l1-14M9 7V4h6v3';
@Component({
selector: 'demo-root',
imports: [OgeSpeedDial],
changeDetection: ChangeDetectionStrategy.OnPush,
template: `
<!-- The APG menu button pattern on a FAB: Enter, Space, a click or the
arrow pointing along the dial opens it on the nearest action; the
arrows move and wrap, Escape returns focus to the FAB. -->
<div style="position: relative; height: 288px">
<oge-speed-dial
label="Share options"
positionMode="absolute"
[items]="actions"
[(opened)]="opened"
(itemClick)="run($event)"
/>
</div>
<p>Last action: {{ last() }}</p>
`,
})
export class Demo {
protected readonly opened = signal(false);
protected readonly last = signal('none');
protected readonly actions: OgeSpeedDialItem[] = [
{ key: 'mail', label: 'Email', icon: MAIL },
{ key: 'print', label: 'Print', icon: PRINT },
{ key: 'share', label: 'Copy link', icon: SHARE },
{ key: 'delete', label: 'Delete', icon: TRASH, severity: 'danger' },
];
protected run(event: OgeSpeedDialItemClickEvent): void {
this.last.set(event.item.label);
}
}Directions & label modes
direction overrides the default (up from a bottom FAB, down from a top one). labelMode is hover (beside the hovered or focused action, always on touch screens), always, or none — the label then stays the accessible name only. A disabled action is aria-disabled and skipped by the arrows.
import { ChangeDetectionStrategy, Component } from '@angular/core';
import { OgeSpeedDial } from '@oge-ui/buttons';
import type { OgeSpeedDialItem } from '@oge-ui/buttons';
const DOC = 'M6 3h9l4 4v14H6zM14 3v5h5';
const IMAGE = 'M4 5h16v14H4zM4 16l5-5 4 4 3-3 4 4';
const MAIL = 'M3 6h18v12H3zM3 6l9 7 9-7';
@Component({
selector: 'demo-root',
imports: [OgeSpeedDial],
changeDetection: ChangeDetectionStrategy.OnPush,
template: `
<!-- direction overrides the default (away from the pinned edge);
labelMode "always" shows every label while open, "none" keeps them as
accessible names only. -->
<div style="position: relative; height: 240px">
<oge-speed-dial
label="New file"
positionMode="absolute"
position="bottom-end"
direction="start"
labelMode="none"
[items]="files"
/>
<oge-speed-dial
label="Insert"
positionMode="absolute"
position="top-start"
labelMode="always"
size="sm"
[items]="files"
/>
</div>
`,
})
export class Demo {
protected readonly files: OgeSpeedDialItem[] = [
{ key: 'doc', label: 'Document', icon: DOC },
{ key: 'image', label: 'Image', icon: IMAGE },
{ key: 'mail', label: 'Email draft', icon: MAIL, disabled: true },
];
}Hover mode
openMode="hover" also opens the dial while a mouse hovers it — never on touch, where a tap is the only gesture. Hover does not move focus; a click still opens it with focus on the first action.
import { ChangeDetectionStrategy, Component } from '@angular/core';
import { OgeSpeedDial } from '@oge-ui/buttons';
import type { OgeSpeedDialItem } from '@oge-ui/buttons';
const MAIL = 'M3 6h18v12H3zM3 6l9 7 9-7';
const SHARE = 'M4 12v7h16v-7M12 3v12M7 8l5-5 5 5';
@Component({
selector: 'demo-root',
imports: [OgeSpeedDial],
changeDetection: ChangeDetectionStrategy.OnPush,
template: `
<!-- openMode="hover" also opens while a mouse hovers the dial (never
on touch, where a tap is the only gesture). Hover does not move focus;
a click still opens it with focus on the first action. -->
<div style="position: relative; height: 224px">
<oge-speed-dial
label="Quick actions"
openMode="hover"
positionMode="absolute"
severity="normal"
[items]="quick"
/>
</div>
`,
})
export class Demo {
protected readonly quick: OgeSpeedDialItem[] = [
{ key: 'share', label: 'Share', icon: SHARE },
{ key: 'mail', label: 'Email', icon: MAIL },
];
}