Usage
Fab represents the single primary action on a screen. Import it from @udixio/ui-react, @udixio/ui-angular or @udixio/ui-svelte; label and icon are both required, and arbitrary children are not accepted.
Positioning is owned by the surrounding layout: the component never places itself. Each size keeps a 48 px touch target and a visible focus treatment.
Color families
Six treatments are available: primary, secondary and tertiary, plus their primaryContainer, secondaryContainer and tertiaryContainer counterparts. The family is independent of every other property — any of them can be compact or extended, at any size.
import { Fab } from '@udixio/ui-react';
import { iAdd } from '@udixio/icons-rounded-400/add';
export default function FabVariantsReact() {
return (
<div className="grid grid-cols-3 items-end justify-items-end gap-4">
<Fab label="Primary" icon={iAdd} variant="primary" extended />
<Fab label="Secondary" icon={iAdd} variant="secondary" extended />
<Fab label="Tertiary" icon={iAdd} variant="tertiary" extended />
<Fab
label="Primary container"
icon={iAdd}
variant="primaryContainer"
extended
/>
<Fab
label="Secondary container"
icon={iAdd}
variant="secondaryContainer"
extended
/>
<Fab
label="Tertiary container"
icon={iAdd}
variant="tertiaryContainer"
extended
/>
</div>
);
}import { ChangeDetectionStrategy, Component } from '@angular/core';
import { Fab } from '@udixio/ui-angular';
import { iAdd } from '@udixio/icons-rounded-400/add';
@Component({
selector: 'docs-fab-variants-angular',
standalone: true,
imports: [Fab],
changeDetection: ChangeDetectionStrategy.OnPush,
template: `
<div class="grid grid-cols-3 items-end justify-items-end gap-4">
<udx-fab label="Primary" [icon]="addIcon" variant="primary" extended />
<udx-fab
label="Secondary"
[icon]="addIcon"
variant="secondary"
extended
/>
<udx-fab label="Tertiary" [icon]="addIcon" variant="tertiary" extended />
<udx-fab
label="Primary container"
[icon]="addIcon"
variant="primaryContainer"
extended
/>
<udx-fab
label="Secondary container"
[icon]="addIcon"
variant="secondaryContainer"
extended
/>
<udx-fab
label="Tertiary container"
[icon]="addIcon"
variant="tertiaryContainer"
extended
/>
</div>
`,
})
export class FabVariantsAngular {
protected readonly addIcon = iAdd;
}<script lang="ts">
import { Fab } from '@udixio/ui-svelte';
import { iAdd } from '@udixio/icons-rounded-400/add';
</script>
<div class="grid grid-cols-3 items-end justify-items-end gap-4">
<Fab label="Primary" icon={iAdd} variant="primary" extended />
<Fab label="Secondary" icon={iAdd} variant="secondary" extended />
<Fab label="Tertiary" icon={iAdd} variant="tertiary" extended />
<Fab label="Primary container" icon={iAdd} variant="primaryContainer" extended />
<Fab label="Secondary container" icon={iAdd} variant="secondaryContainer" extended />
<Fab label="Tertiary container" icon={iAdd} variant="tertiaryContainer" extended />
</div>Sizes
small, medium (the default) and large give 56, 80 and 96 px containers with 24, 28 and 36 px icons. A compact fab is always square; an extended one keeps the height of its size and takes its width from the label.
import { Fab } from '@udixio/ui-react';
import { iEdit } from '@udixio/icons-rounded-400/edit';
export default function FabSizesReact() {
return (
<div className="flex flex-wrap items-end justify-end gap-4">
<Fab label="Small" icon={iEdit} size="small" />
<Fab label="Medium" icon={iEdit} size="medium" />
<Fab label="Large" icon={iEdit} size="large" />
</div>
);
}import { ChangeDetectionStrategy, Component } from '@angular/core';
import { Fab } from '@udixio/ui-angular';
import { iEdit } from '@udixio/icons-rounded-400/edit';
@Component({
selector: 'docs-fab-sizes-angular',
standalone: true,
imports: [Fab],
changeDetection: ChangeDetectionStrategy.OnPush,
template: `
<div class="flex flex-wrap items-end justify-end gap-4">
<udx-fab label="Small" [icon]="editIcon" size="small" />
<udx-fab label="Medium" [icon]="editIcon" size="medium" />
<udx-fab label="Large" [icon]="editIcon" size="large" />
</div>
`,
})
export class FabSizesAngular {
protected readonly editIcon = iEdit;
}<script lang="ts">
import { Fab } from '@udixio/ui-svelte';
import { iEdit } from '@udixio/icons-rounded-400/edit';
</script>
<div class="flex flex-wrap items-end justify-end gap-4">
<Fab label="Small" icon={iEdit} size="small" />
<Fab label="Medium" icon={iEdit} size="medium" />
<Fab label="Large" icon={iEdit} size="large" />
</div>Extended and compact
extended reveals the label beside the icon. Toggling it animates the pill open and closed: a shared controller diffs the label’s width and opacity with Anime.js Layout while the container’s padding and gap transition alongside it, on the same duration and easing. Under prefers-reduced-motion the change is applied instantly instead.
In compact mode, label remains the accessible name without creating visible text. The label element stays mounted, collapsed to zero width and hidden from assistive technology, so the transition never has to unmount anything.
A compact fab also names its icon in a tooltip on hover and focus, which Material 3 requires of every fab on the web — you get it without asking. Since that text repeats the accessible name, the tooltip does not also describe the target, so a screen reader announces it once. tooltip takes over when the hint should say more than the label (tooltip="Create a note (N)", which then does describe the target), and tooltip={false} removes it — an accessibility exception worth justifying. An extended fab never shows one: its label is already visible.
import { useState } from 'react';
import { Button, Fab } from '@udixio/ui-react';
import { iEdit } from '@udixio/icons-rounded-400/edit';
export default function FabExtendedReact() {
const [extended, setExtended] = useState(true);
return (
<div className="flex flex-col items-end gap-6">
<Button
label={extended ? 'Collapse the fab' : 'Extend the fab'}
variant="outlined"
onClick={() => setExtended((current) => !current)}
/>
<Fab label="Compose" icon={iEdit} extended={extended} />
</div>
);
}import { ChangeDetectionStrategy, Component, signal } from '@angular/core';
import { Button, Fab } from '@udixio/ui-angular';
import { iEdit } from '@udixio/icons-rounded-400/edit';
@Component({
selector: 'docs-fab-extended-angular',
standalone: true,
imports: [Button, Fab],
changeDetection: ChangeDetectionStrategy.OnPush,
template: `
<div class="flex flex-col items-end gap-6">
<udx-button
[label]="extended() ? 'Collapse the fab' : 'Extend the fab'"
variant="outlined"
(click)="extended.set(!extended())"
/>
<udx-fab label="Compose" [icon]="editIcon" [extended]="extended()" />
</div>
`,
})
export class FabExtendedAngular {
protected readonly extended = signal(true);
protected readonly editIcon = iEdit;
}<script lang="ts">
import { Button, Fab } from '@udixio/ui-svelte';
import { iEdit } from '@udixio/icons-rounded-400/edit';
let extended = $state(true);
</script>
<div class="flex flex-col items-end gap-6">
<Button
label={extended ? 'Collapse the fab' : 'Extend the fab'}
variant="outlined"
onclick={() => (extended = !extended)}
/>
<Fab label="Compose" icon={iEdit} {extended} />
</div>Link or action
Without href, the component renders a native button and defaults to type="button", so it never submits a surrounding form by accident. With href, it renders a native link, where aria-current marks the destination already open.
No action yet
import { useState } from 'react';
import { Fab } from '@udixio/ui-react';
import { iAdd } from '@udixio/icons-rounded-400/add';
import { iShare } from '@udixio/icons-rounded-400/share';
export default function FabActionsReact() {
const [message, setMessage] = useState('No action yet');
return (
<div className="flex flex-col items-end gap-4">
<div className="flex flex-wrap items-end justify-end gap-4">
<Fab
label="Create"
icon={iAdd}
extended
onClick={() => setMessage('Action run')}
/>
<Fab
label="Share this page"
icon={iShare}
variant="tertiaryContainer"
href="/components/fab/overview"
aria-current="page"
extended
/>
</div>
<p aria-live="polite" className="text-body-medium">
{message}
</p>
</div>
);
}No action yet
import { ChangeDetectionStrategy, Component, signal } from '@angular/core';
import { Fab } from '@udixio/ui-angular';
import { iAdd } from '@udixio/icons-rounded-400/add';
import { iShare } from '@udixio/icons-rounded-400/share';
@Component({
selector: 'docs-fab-actions-angular',
standalone: true,
imports: [Fab],
changeDetection: ChangeDetectionStrategy.OnPush,
template: `
<div class="flex flex-col items-end gap-4">
<div class="flex flex-wrap items-end justify-end gap-4">
<udx-fab
label="Create"
[icon]="addIcon"
extended
(click)="message.set('Action run')"
/>
<udx-fab
label="Share this page"
[icon]="shareIcon"
variant="tertiaryContainer"
href="/components/fab/overview"
aria-current="page"
extended
/>
</div>
<p aria-live="polite" class="text-body-medium">{{ message() }}</p>
</div>
`,
})
export class FabActionsAngular {
protected readonly addIcon = iAdd;
protected readonly shareIcon = iShare;
protected readonly message = signal('No action yet');
}No action yet
<script lang="ts">
import { Fab } from '@udixio/ui-svelte';
import { iAdd } from '@udixio/icons-rounded-400/add';
import { iShare } from '@udixio/icons-rounded-400/share';
let message = $state('No action yet');
</script>
<div class="flex flex-col items-end gap-4">
<div class="flex flex-wrap items-end justify-end gap-4">
<Fab label="Create" icon={iAdd} extended onclick={() => (message = 'Action run')} />
<Fab
label="Share this page"
icon={iShare}
variant="tertiaryContainer"
href="/components/fab/overview"
aria-current="page"
extended
/>
</div>
<p aria-live="polite" class="text-body-medium">{message}</p>
</div>Disabled
disabled applies to both renderings. A disabled button uses the native attribute; a disabled link drops its href, carries aria-disabled, leaves the tab order, and swallows clicks — a native anchor has no disabled state of its own.
import { Fab } from '@udixio/ui-react';
import { iAdd } from '@udixio/icons-rounded-400/add';
export default function FabStatesReact() {
return (
<div className="flex flex-wrap items-end justify-end gap-4">
<Fab label="Disabled action" icon={iAdd} disabled extended />
<Fab
label="Disabled link"
icon={iAdd}
href="/components/fab/overview"
disabled
extended
/>
<Fab label="Disabled compact" icon={iAdd} disabled />
</div>
);
}import { ChangeDetectionStrategy, Component } from '@angular/core';
import { Fab } from '@udixio/ui-angular';
import { iAdd } from '@udixio/icons-rounded-400/add';
@Component({
selector: 'docs-fab-states-angular',
standalone: true,
imports: [Fab],
changeDetection: ChangeDetectionStrategy.OnPush,
template: `
<div class="flex flex-wrap items-end justify-end gap-4">
<udx-fab label="Disabled action" [icon]="addIcon" disabled extended />
<udx-fab
label="Disabled link"
[icon]="addIcon"
href="/components/fab/overview"
disabled
extended
/>
<udx-fab label="Disabled compact" [icon]="addIcon" disabled />
</div>
`,
})
export class FabStatesAngular {
protected readonly addIcon = iAdd;
}<script lang="ts">
import { Fab } from '@udixio/ui-svelte';
import { iAdd } from '@udixio/icons-rounded-400/add';
</script>
<div class="flex flex-wrap items-end justify-end gap-4">
<Fab label="Disabled action" icon={iAdd} disabled extended />
<Fab
label="Disabled link"
icon={iAdd}
href="/components/fab/overview"
disabled
extended
/>
<Fab label="Disabled compact" icon={iAdd} disabled />
</div>Migrating to 3.0
| Before | 3.0 |
|---|---|
text children as the label | required label |
| implicit tooltip | compose Tooltip explicitly |
| missing action type | type="button" by default |
documented variant="surface" | use a supported container variant |