FabMenu
The floating action button menu opens from a FAB to display multiple related actions.
Usage
FabMenu reveals a short group of related primary actions from one floating action button. Its actions model is shared by React, Angular, and Svelte. Use the external control below to replay the opening and closing choreography; the status region makes controlled state and selection observable without opening the browser console.
Menu: closed
Last action: None
import { useState } from 'react';
import { Button, FabMenu, type FabMenuAction } from '@udixio/ui-react';
import { iAdd } from '@udixio/icons-rounded-400/add';
import { iEdit } from '@udixio/icons-rounded-400/edit';
import { iShare } from '@udixio/icons-rounded-400/share';
const actions: FabMenuAction[] = [
{ id: 'document', label: 'Edit document', icon: iEdit },
{ id: 'share', label: 'Share document', icon: iShare },
];
export default function FabMenuActionsReact() {
const [open, setOpen] = useState(false);
const [lastAction, setLastAction] = useState('None');
return (
<div className="grid min-h-80 w-full content-between gap-8">
<div
className="rounded-xl border border-outline p-4"
role="status"
aria-live="polite"
>
<p className="text-title-medium">Menu: {open ? 'open' : 'closed'}</p>
<p className="text-body-medium text-on-surface-variant">
Last action: {lastAction}
</p>
</div>
<div className="flex flex-wrap items-end justify-between gap-4">
<Button
label={open ? 'Close from owner' : 'Open from owner'}
variant="outlined"
onClick={() => setOpen((current) => !current)}
/>
<FabMenu
label="Create"
icon={iAdd}
actions={actions}
size="large"
extended
open={open}
onOpenChange={setOpen}
onActionSelect={(action) => setLastAction(action.label)}
/>
</div>
</div>
);
}Menu: closed
Last action: None
import { ChangeDetectionStrategy, Component, signal } from '@angular/core';
import {
Button,
FabMenu,
type FabMenuAction,
type FabMenuActionSelectEvent,
} from '@udixio/ui-angular';
import { iAdd } from '@udixio/icons-rounded-400/add';
import { iEdit } from '@udixio/icons-rounded-400/edit';
import { iShare } from '@udixio/icons-rounded-400/share';
@Component({
selector: 'docs-fab-menu-actions-angular',
standalone: true,
imports: [Button, FabMenu],
changeDetection: ChangeDetectionStrategy.OnPush,
template: `
<div class="grid min-h-80 w-full content-between gap-8">
<div
class="rounded-xl border border-outline p-4"
role="status"
aria-live="polite"
>
<p class="text-title-medium">Menu: {{ open() ? 'open' : 'closed' }}</p>
<p class="text-body-medium text-on-surface-variant">
Last action: {{ lastAction() }}
</p>
</div>
<div class="flex flex-wrap items-end justify-between gap-4">
<udx-button
[label]="open() ? 'Close from owner' : 'Open from owner'"
variant="outlined"
(click)="toggleMenu()"
/>
<udx-fab-menu
label="Create"
[icon]="addIcon"
[actions]="actions"
size="large"
extended
[open]="open()"
(openChange)="handleOpenChange($event)"
(actionSelect)="handleAction($event)"
/>
</div>
</div>
`,
})
export class FabMenuActionsAngular {
protected readonly addIcon = iAdd;
protected readonly actions: FabMenuAction[] = [
{ id: 'document', label: 'Edit document', icon: iEdit },
{ id: 'share', label: 'Share document', icon: iShare },
];
protected readonly open = signal(false);
protected readonly lastAction = signal('None');
protected toggleMenu(): void {
this.open.update((current) => !current);
}
protected handleOpenChange(open: boolean): void {
this.open.set(open);
}
protected handleAction(event: FabMenuActionSelectEvent): void {
this.lastAction.set(event.action.label);
}
}Menu: closed
Last action: None
<script lang="ts">
import type { FabMenuAction } from '@udixio/core';
import { iAdd } from '@udixio/icons-rounded-400/add';
import { iEdit } from '@udixio/icons-rounded-400/edit';
import { iShare } from '@udixio/icons-rounded-400/share';
import { Button, FabMenu } from '@udixio/ui-svelte';
const actions: FabMenuAction[] = [
{ id: 'document', label: 'Edit document', icon: iEdit },
{ id: 'share', label: 'Share document', icon: iShare },
];
let open = $state(false);
let lastAction = $state('None');
</script>
<div class="grid min-h-80 w-full content-between gap-8">
<div class="rounded-xl border border-outline p-4" role="status" aria-live="polite">
<p class="text-title-medium">Menu: {open ? 'open' : 'closed'}</p>
<p class="text-body-medium text-on-surface-variant">Last action: {lastAction}</p>
</div>
<div class="flex flex-wrap items-end justify-between gap-4">
<Button label={open ? 'Close from owner' : 'Open from owner'} variant="outlined" onclick={() => (open = !open)} />
<FabMenu
label="Create"
icon={iAdd}
{actions}
size="large"
extended
bind:open
onActionSelect={(action) => (lastAction = action.label)}
/>
</div>
</div>Open the menu with its trigger, then select an action or press Escape. Focus returns to the trigger when the menu closes.
Primary
The default color family is appropriate for the screen’s main creation flow. This large trigger keeps five related actions immediately available.
import { FabMenu, type FabMenuAction } from '@udixio/ui-react';
import { iAdd } from '@udixio/icons-rounded-400/add';
const actions: FabMenuAction[] = [
{ id: 'document', label: 'New document' },
{ id: 'folder', label: 'New folder' },
{ id: 'upload', label: 'Upload file' },
{ id: 'scan', label: 'Scan document' },
{ id: 'import', label: 'Import content' },
];
export default function FabMenuPrimaryReact() {
return (
<div className="flex min-h-[28rem] w-full items-end justify-center">
<FabMenu label="Create" icon={iAdd} actions={actions} size="large" />
</div>
);
}import { ChangeDetectionStrategy, Component } from '@angular/core';
import { FabMenu, type FabMenuAction } from '@udixio/ui-angular';
import { iAdd } from '@udixio/icons-rounded-400/add';
@Component({
selector: 'docs-fab-menu-primary-angular',
standalone: true,
imports: [FabMenu],
changeDetection: ChangeDetectionStrategy.OnPush,
template: `
<div class="flex min-h-[28rem] w-full items-end justify-center">
<udx-fab-menu
label="Create"
[icon]="addIcon"
[actions]="actions"
size="large"
/>
</div>
`,
})
export class FabMenuPrimaryAngular {
protected readonly addIcon = iAdd;
protected readonly actions: FabMenuAction[] = [
{ id: 'document', label: 'New document' },
{ id: 'folder', label: 'New folder' },
{ id: 'upload', label: 'Upload file' },
{ id: 'scan', label: 'Scan document' },
{ id: 'import', label: 'Import content' },
];
}<script lang="ts">
import type { FabMenuAction } from '@udixio/core';
import { iAdd } from '@udixio/icons-rounded-400/add';
import { FabMenu } from '@udixio/ui-svelte';
const actions: FabMenuAction[] = [
{ id: 'document', label: 'New document' },
{ id: 'folder', label: 'New folder' },
{ id: 'upload', label: 'Upload file' },
{ id: 'scan', label: 'Scan document' },
{ id: 'import', label: 'Import content' },
];
</script>
<div class="flex min-h-[28rem] w-full items-end justify-center">
<FabMenu label="Create" icon={iAdd} {actions} size="large" />
</div>Secondary extended
Use an extended trigger when its action benefits from a persistent visible label. Opening still contracts it to the medium close control.
import { FabMenu, type FabMenuAction } from '@udixio/ui-react';
import { iAdd } from '@udixio/icons-rounded-400/add';
const actions: FabMenuAction[] = [
{ id: 'note', label: 'Add note' },
{ id: 'photo', label: 'Take photo' },
{ id: 'attachment', label: 'Attach file' },
{ id: 'recording', label: 'Record audio' },
{ id: 'location', label: 'Add location' },
];
export default function FabMenuSecondaryReact() {
return (
<div className="flex min-h-[28rem] w-full items-end justify-center">
<FabMenu
label="Add content"
icon={iAdd}
actions={actions}
variant="secondary"
size="large"
extended
/>
</div>
);
}import { ChangeDetectionStrategy, Component } from '@angular/core';
import { FabMenu, type FabMenuAction } from '@udixio/ui-angular';
import { iAdd } from '@udixio/icons-rounded-400/add';
@Component({
selector: 'docs-fab-menu-secondary-angular',
standalone: true,
imports: [FabMenu],
changeDetection: ChangeDetectionStrategy.OnPush,
template: `
<div class="flex min-h-[28rem] w-full items-end justify-center">
<udx-fab-menu
label="Add content"
[icon]="addIcon"
[actions]="actions"
variant="secondary"
size="large"
extended
/>
</div>
`,
})
export class FabMenuSecondaryAngular {
protected readonly addIcon = iAdd;
protected readonly actions: FabMenuAction[] = [
{ id: 'note', label: 'Add note' },
{ id: 'photo', label: 'Take photo' },
{ id: 'attachment', label: 'Attach file' },
{ id: 'recording', label: 'Record audio' },
{ id: 'location', label: 'Add location' },
];
}<script lang="ts">
import type { FabMenuAction } from '@udixio/core';
import { iAdd } from '@udixio/icons-rounded-400/add';
import { FabMenu } from '@udixio/ui-svelte';
const actions: FabMenuAction[] = [
{ id: 'note', label: 'Add note' },
{ id: 'photo', label: 'Take photo' },
{ id: 'attachment', label: 'Attach file' },
{ id: 'recording', label: 'Record audio' },
{ id: 'location', label: 'Add location' },
];
</script>
<div class="flex min-h-[28rem] w-full items-end justify-center">
<FabMenu label="Add content" icon={iAdd} {actions} variant="secondary" size="large" extended />
</div>Tertiary
The tertiary family can distinguish a contextual action group while preserving the same five-action choreography and large closed trigger.
import { FabMenu, type FabMenuAction } from '@udixio/ui-react';
import { iAdd } from '@udixio/icons-rounded-400/add';
const actions: FabMenuAction[] = [
{ id: 'message', label: 'New message' },
{ id: 'reply', label: 'Reply' },
{ id: 'forward', label: 'Forward' },
{ id: 'archive', label: 'Archive' },
{ id: 'reminder', label: 'Add reminder' },
];
export default function FabMenuTertiaryReact() {
return (
<div className="flex min-h-[28rem] w-full items-end justify-center">
<FabMenu
label="Compose"
icon={iAdd}
actions={actions}
variant="tertiary"
size="large"
/>
</div>
);
}import { ChangeDetectionStrategy, Component } from '@angular/core';
import { FabMenu, type FabMenuAction } from '@udixio/ui-angular';
import { iAdd } from '@udixio/icons-rounded-400/add';
@Component({
selector: 'docs-fab-menu-tertiary-angular',
standalone: true,
imports: [FabMenu],
changeDetection: ChangeDetectionStrategy.OnPush,
template: `
<div class="flex min-h-[28rem] w-full items-end justify-center">
<udx-fab-menu
label="Compose"
[icon]="addIcon"
[actions]="actions"
variant="tertiary"
size="large"
/>
</div>
`,
})
export class FabMenuTertiaryAngular {
protected readonly addIcon = iAdd;
protected readonly actions: FabMenuAction[] = [
{ id: 'message', label: 'New message' },
{ id: 'reply', label: 'Reply' },
{ id: 'forward', label: 'Forward' },
{ id: 'archive', label: 'Archive' },
{ id: 'reminder', label: 'Add reminder' },
];
}<script lang="ts">
import type { FabMenuAction } from '@udixio/core';
import { iAdd } from '@udixio/icons-rounded-400/add';
import { FabMenu } from '@udixio/ui-svelte';
const actions: FabMenuAction[] = [
{ id: 'message', label: 'New message' },
{ id: 'reply', label: 'Reply' },
{ id: 'forward', label: 'Forward' },
{ id: 'archive', label: 'Archive' },
{ id: 'reminder', label: 'Add reminder' },
];
</script>
<div class="flex min-h-[28rem] w-full items-end justify-center">
<FabMenu label="Compose" icon={iAdd} {actions} variant="tertiary" size="large" />
</div>Controlled state and action events
Use open with onOpenChange (React), openChange (Angular), or bind:open (Svelte) for controlled state. defaultOpen initializes uncontrolled state. Selection emits onActionSelect(action, index) in React and actionSelect with { action, index } in Angular; Svelte receives (action, index) in onActionSelect before closing.
Actions may be buttons or links and may include an icon or disabled state.
Motion
Actions enter progressively from the trigger and collapse in reverse order. When the menu opens, an extended trigger contracts to its icon-only close control. The close control is always medium (56 px), including when the closed trigger is small or large, matching the original FabMenu choreography. The closed trigger keeps its layout footprint while the control remains anchored to its top-right corner. Its container also morphs from the container color to the selected color family; closing reverses these transitions. The choreography uses the framework-independent Motion JavaScript controller in @udixio/core/dom, so React, Angular, and Svelte share the same timing, interruption, cleanup, and reduced-motion behavior. No motion/react adapter is required.
Accessibility and interaction
The trigger exposes aria-expanded and aria-controls. Opening moves focus to the first enabled action. Escape closes the group and restores trigger focus; selection and outside press also close it. Actions retain ordinary button/link semantics inside a labelled group instead of claiming full ARIA menu behavior.
Use closeLabel and actionsLabel when the generated English defaults are not appropriate for the product language.
Migrating to 3.0
| Before | 3.0 |
|---|---|
<FabMenu><Button>…</Button></FabMenu> | <FabMenu actions={[…]} /> |
| child callbacks | onActionSelect(action, index) / actionSelect |
React-only motion/react choreography | shared Motion JavaScript, focus, and dismissal |
| explicit close child | closeIcon and closeLabel |