Fab
Fab is a circular button that calls attention to a screen's single most important action, such as composing a new message or adding an item to a list. It shares its visual styling and behavior with the regular button, so it can be extended into a wider pill that reveals a text label on hover or focus, and it can dock itself to a corner of the viewport with no extra layout work.
Dependencies
When to use
Use a FAB for the single most important, most frequent action on a screen, such as composing a new message or adding an item, not for a list of equally-weighted actions. A page should have at most one. See Alternatives below for several related actions instead of just one.
Install & usage
Pick a framework in the toolbar above and these snippets adapt.
npm install @eunomia/elements
import "@eunomia/elements/fab.js";
import "@eunomia/elements/icon.js";
<eun-fab icon="add" aria-label="Create"></eun-fab>
Importing the file registers <eun-fab> as a custom element, with no
further setup needed. It works with any framework, or none, since it's a
standard web component.
npm install @eunomia/elements
<script type="module">
import "@eunomia/elements/fab.js";
import "@eunomia/elements/icon.js";
</script>
<eun-fab icon="add" aria-label="Create"></eun-fab>
npm install @eunomia/elements
import "@eunomia/elements/fab.js";
import "@eunomia/elements/icon.js";
function CreateFab() {
return (
<eun-fab
icon="add"
aria-label="Create"
onclick={() => console.log("create")}
/>
);
}
npm install @eunomia/elements
"use client";
import "@eunomia/elements/fab.js";
import "@eunomia/elements/icon.js";
export function CreateFab() {
return (
<eun-fab
icon="add"
aria-label="Create"
onclick={() => console.log("create")}
/>
);
}
npm install @eunomia/elements
<script setup>
import "@eunomia/elements/fab.js";
import "@eunomia/elements/icon.js";
</script>
<template>
<eun-fab
icon="add"
aria-label="Create"
@click="() => console.log('create')"
/>
</template>
npm install @eunomia/elements
import { CUSTOM_ELEMENTS_SCHEMA, Component } from "@angular/core";
import "@eunomia/elements/fab.js";
import "@eunomia/elements/icon.js";
@Component({
selector: "app-create-fab",
template: `<eun-fab
icon="add"
aria-label="Create"
(click)="onCreate()"
></eun-fab>`,
schemas: [CUSTOM_ELEMENTS_SCHEMA],
})
export class CreateFabComponent {
onCreate() {
console.log("create");
}
}
Alternatives
eun-buttonGuidance
- Reserve
eun-fabfor one single, primary, frequently-used action per screen: "compose", "create", "add" - Wire it to a target with
command/commandforinstead of a click handler whenever the FAB doesn't need to run other logic first, preferred over the Popover API's ownpopovertarget: see Invoker Commands API below - Always set
aria-labelwhenlabelisn't set, since an icon alone has no accessible name - Set
positionwhen the FAB should stay docked to a viewport corner while the page scrolls, and leave it unset to place the FAB yourself inside normal page flow (e.g. a toolbar) - Use
labelwhen the icon alone might be ambiguous, so hovering/focusing confirms the action in words
- Using more than one
eun-fabon the same screen, see Alternatives above for several related actions instead - Leaving both
labelandaria-labelunset: the button would have no accessible name at all
Live testing
Properties
Fab <eun-fab>
Attributes
| Name | Type | Default | Description |
|---|---|---|---|
| aria-label | string | — | The accessible label for the button. Required when label isn't set, since an icon alone has no accessible name, and recommended even when it is if the label text alone doesn't describe the action |
| icon | EunomiaIconName | — | The icon displayed at all times |
| label | string | — | An optional text label, revealed on hover or focus by the slide animation. Omit it for an icon only button |
| position | 'bottom-right' | 'bottom-left' | 'top-right' | 'top-left' | — | Fixes the button to a corner of the viewport. Left unset, the button follows the normal document flow |
| loading-label | string | 'Loading…' | The accessible label announced while the button is loading |
| active | boolean | false | Indicates whether the component is on disabled state or not. By default, set to false. |
Import the exact TypeScript type behind any property above, see
Properties
JS-only — no matching HTML attribute, set these from a script or a template binding.
| Name | Type | Default | Description |
|---|---|---|---|
| variant | 'primary' | 'secondary' | 'critical' | 'success' | 'warning' | 'primary' | The color theme applied to the button |
| size | string | 'm' | The size of the button |
| href | string | — | The URL the button navigates to. When set, the button renders as a link instead of a button |
| target | '_blank' | '_self' | '_parent' | '_top' | — | Where to open the linked URL. Only used when href is set. Opening in a new tab automatically announces it to assistive technology, and adds an external link icon when label is also set |
| download | string | — | The filename used when the linked URL is downloaded instead of navigated to. Only used when href is set |
| loading | boolean | false | Whether the button shows a loading indicator. Like disabled, it blocks interaction, but keeps the icon and label in place so the button doesn't change size |
| command | string | — | The command sent to the element targeted by commandFor when the button is clicked, following the Invoker Commands API |
| commandFor | string | — | The id of the element that command is sent to |
| type | string | "button" | The button type: "submit", "reset", or "button". Determines behavior when clicked inside a form. |
| rounded | boolean | false | Indicates whether the button should take a round shape or not. |
| slide | boolean | false | Indicates whether the button should handle a slide mechanism. |
Slots
| Name | Description |
|---|---|
| icon | Replaces the icon entirely with any content, not just an icon element. Falls back to icon when empty |
Events
| Name | Type | Description |
|---|---|---|
| click | Event | Fired when the button is activated by mouse, Enter, or Space |
| eunnavigate | NavigationEvent | Fired before following href, so a client-side router can intercept the navigation. Not fired for downloads, and cancelable |
Every event above follows the same naming convention, covered in
CSS custom properties
| Name | Description |
|---|---|
| --fab-background | Sets the background color |
| --fab-background-hover | Sets the background color on hover |
| --fab-text | Sets the color of the text and icon |
| --fab-focus-outline | Sets the color of the focus outline |
| --fab-transition | Sets the duration of the slide animation and the hover and focus color changes |
| --fab-z-index | Sets the stack order of a positioned button |
| --fab-offset | Sets the distance from the viewport edge for a positioned button |
Variants
Same variants as primary (default), secondary, critical, success, warning.
<eun-fab icon="add" aria-label="Create"></eun-fab>
<eun-fab icon="add" variant="secondary" aria-label="Create"></eun-fab>
<eun-fab icon="delete" variant="critical" aria-label="Delete"></eun-fab>
<eun-fab icon="favorite" variant="success" aria-label="Save"></eun-fab>
<eun-fab icon="warning" variant="warning" aria-label="Warn"></eun-fab>
Part of the shared vocabulary covered in
Custom icon
Need something other than a eun-icon, or a different icon library
entirely? Slot your own content into icon instead. It replaces the
prop-driven icon entirely.
<eun-fab aria-label="Create">
<svg
slot="icon"
width="20"
height="20"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
>
<path d="M12 5v14M5 12h14"></path>
</svg>
</eun-fab>
Invoker Commands API
Setting command and commandfor turns the FAB into an invoker: no
click handler needed, the target picks up the command itself. Prefer
this over wiring a click handler yourself, and over the Popover API's
own popovertarget, which only ever covers popover-attributed
targets. Modal and Drawer both understand show-modal, close, and
request-close out of the box.
Write a new message.
<eun-modal id="compose-modal" heading="Compose message">
<p>Write a new message.</p>
</eun-modal>
<eun-fab
icon="edit"
aria-label="Compose"
command="show-modal"
commandfor="compose-modal"
></eun-fab>
See the
Extended
Set label to make the FAB extendable: it stays a plain circle until
hovered/focused, then slides open to reveal the label, the same slide
behavior label is set. Hover or
focus the button below to see it expand.
<eun-fab icon="add" label="Create"></eun-fab>
Without label, the FAB is always icon-only: set aria-label in that
case, since an icon alone has no accessible name.
Sizes
Five sizes are available, matching xs, s, m (default), l, xl.
<eun-fab icon="add" size="xs" aria-label="Create"></eun-fab>
<eun-fab icon="add" size="s" aria-label="Create"></eun-fab>
<eun-fab icon="add" aria-label="Create"></eun-fab>
<eun-fab icon="add" size="l" aria-label="Create"></eun-fab>
<eun-fab icon="add" size="xl" aria-label="Create"></eun-fab>
Fixed positioning
Set position (bottom-right, bottom-left, top-right, top-left) to
dock the FAB to a corner of the viewport, with no extra CSS needed.
<eun-fab icon="add" position="bottom-right" aria-label="Create"></eun-fab>
Distance from the edge defaults to --eun-space-l (24px) and is
customizable through --fab-offset, while the stacking order defaults to
40 and is customizable through --fab-z-index.
Leave position unset to place the FAB yourself: it then behaves like a
normal inline element wherever you put it in the page flow (e.g. inside a
toolbar, as in every other demo on this page).
Disabled
<eun-fab icon="add" disabled aria-label="Create"></eun-fab>
Loading
Setting loading blocks interaction like disabled, but keeps the
icon/label in place (invisible, so the button's size doesn't change)
with a
<eun-fab icon="add" loading aria-label="Create"></eun-fab>
import "@eunomia/elements/loader.js";
Links
Setting href renders the FAB as a native <a> internally instead of a
<button>, exactly like
<eun-fab
icon="open_in_new"
label="Documentation"
href="https://example.com"
></eun-fab>
Opening in a new tab
Setting target="_blank" automatically adds rel="noopener noreferrer"
and appends "(opens in a new tab)" to the accessible name, exactly like
label
is also set, a small external-link icon is appended to it too. An
icon-only FAB (no label) only gets the accessible-name change, since
there's no natural spot for a second icon inside its small circular
footprint without crowding the primary one.
<eun-fab
icon="open_in_new"
label="Documentation"
href="https://example.com"
target="_blank"
aria-label="Documentation"
></eun-fab>
Keyboard interactions
| Key | Action |
|---|---|
Tab / Shift+Tab |
Moves focus to/from the FAB, like any other button |
Enter |
Activates the button (native <button> behavior, or native link navigation when href is set) |
Space |
Activates the button. Ignored when href is set, matching native <a> behavior |
Accessible name
An icon alone has no accessible name: set aria-label whenever label
isn't set, and consider it even when label is set if the label text
alone isn't a sufficient description of the action. Refer to
For a link FAB opening a new tab (href + target="_blank"), "(opens in
a new tab)" is announced automatically, appended directly to aria-label
when one is set, or as a visually-hidden addition to the accessible name
otherwise, so no manual wording is needed either way.
Disabled and loading state
A disabled FAB carries the native disabled attribute (button mode) or
aria-disabled="true" with href omitted (link mode). Either way, it's
removed from the tab order and emits no events, exactly like
While loading, aria-busy="true" is set on the rendered <button>/<a>,
the FAB is removed from the tab order, and the loading indicator exposes
role="status" (via aria-label) plus a redundant visually-hidden aria-live="polite"
announcement, so assistive technology reliably picks up the state change
even if it missed the initial aria-busy update.
Fixed positioning doesn't trap focus or content
position only ever changes CSS (position: fixed plus an offset). It
adds no ARIA role, no focus trap, and doesn't remove any other page content
from the accessibility tree. A positioned FAB sits in the same tab order
position its DOM placement already gives it, so place it thoughtfully in
markup (typically near the end of the document) so Tab doesn't jump to it
unexpectedly in the middle of an unrelated flow.
Reference links