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
| You want… | Prefers |
|---|---|
| Several related, equally-weighted actions rather than one standout action | eun-button |
Guidance
- 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 |
|---|---|---|---|
| fixed-color | boolean | false | Keeps a light or dark button white or black in every theme. Without it, they follow the grey scale, which flips with the active theme. Has no effect on the other colors |
| 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 | — | The accessible label announced while the button is loading. Defaults to the localized string for the current locale (English: "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 |
|---|---|---|---|
| color | 'primary' | 'secondary' | 'critical' | 'success' | 'warning' | 'primary' | The color applied to the button |
| fixedColor | boolean | false | Keeps a light or dark button white or black in every theme. Without it, they follow the grey scale, which flips with the active theme. Has no effect on the other colors |
| 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 |
| locale | string | — | The locale used to resolve the "opens in a new tab" announcement. Defaults to the one set with setDefaultLocale, English otherwise |
| 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. Only reflected once it differs from the default, so an SSR framework doesn't see an attribute it never rendered. |
| 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. |
| hostTabStop | — | Whether the host itself is a tab stop while enabled. `false` for a component whose focusable controls live inside its shadow root. | |
| resolvedLocale | — | This instance's own `locale` when set, the shared default locale otherwise. Used for `Intl` formatting, so dates and numbers follow the same locale as dictionary. | |
| dictionary | — | This instance's locale-resolved string dictionary — see the class doc for the resolution order. |
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 | 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, for every 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
Colors
Same colors as primary (default), secondary, critical, success, warning. The
neutral light, medium and dark colors, and the fixed-color attribute
that keeps light and dark white and black in every theme, work exactly as
they do on the button.
<eun-fab icon="add" aria-label="Create"></eun-fab>
<eun-fab icon="add" color="secondary" aria-label="Create"></eun-fab>
<eun-fab icon="delete" color="critical" aria-label="Delete"></eun-fab>
<eun-fab icon="favorite" color="success" aria-label="Save"></eun-fab>
<eun-fab icon="warning" color="warning" aria-label="Warn"></eun-fab>
Part of the
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>
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.
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>
States
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";
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).
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>
SPA navigation
A link FAB dispatches the same cancelable eunnavigate event as
href, letting an SPA router intercept it for client-side
routing instead of a full page load. It's never dispatched for
download links, since a file download isn't a navigation. See the
Integration
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
closeable
close-on-backdrop
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
Server-side rendering
The fab's circle lives in its shadow root, so a server-rendered fab has
no surface, and a positioned one sits in the page flow, until the element
upgrades. Link button-ssr.css in the page's <head> to draw the circle
and pin it to its corner from the first paint:
<link
rel="stylesheet"
href="node_modules/@eunomia/elements/dist/ssr/button-ssr.css"
/>
It covers every size and color, disabled, position and the --fab-*
custom properties. The icon set by icon appears with the upgrade.
Custom colors
The custom properties that change colors, borders, shadows and corners, at a glance. The API tab lists every property of the component.
| Variable | Sets |
|---|---|
--fab-background |
Sets the background color, for every color |
--fab-background-hover |
Sets the background color on hover |
--fab-focus-outline |
Sets the color of the focus outline |
Keyboard and focus
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 |
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.
Semantics and screen readers
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.
Reference links