DocumentationComponentsTheme Creator
Theme CreatorGitHubIntroduction
InstallationUsageThemingMotion
Components
ButtonGroupSplit ButtonToggleButtonToggleGroupFAB
AI agentsContributing
  1. Components
  2. FAB

FAB

A floating action button — icon-only or extended, with elevation, corner placement, a badge, a stacked group and a speed-dial menu

$ pnpm add @tessinaui/ui

When to use

  • The one primary action of a screen. Create, Compose, Add: the thing most people open this view to do. One FAB per screen.
  • A positive, frequent action that should stay reachable while the content scrolls, pinned to a corner with placement.
  • An extended FAB when the icon alone is ambiguous. Give it a label; use the icon-only form only for a glyph everyone reads the same way.
  • Several related actions behind one anchor. Open a FabMenu speed dial, or stack a few secondary controls in a FabGroup.
  • Floating over media or a map. Use elevation="overlay" or translucent so the control separates from what is beneath it.

When not to use

  • For a screen with no single dominant action. Use a Button in the content or the header instead of forcing one to float.
  • For minor, destructive or toolbar-style actions such as archive, delete, cut or volume. Put them in a Toolbar or an IconButton.
  • For navigation between destinations. Use BottomNav or a Link.
  • As a second or third floating button. Two competing FABs dilute the primary action; group related ones in a FabGroup.
  • For a long list of actions. A speed dial suits a handful; past that, use a DropdownMenu.
  • Beside a filled button in the same area. The two emphasise different things at once; pick one.

Installation

Terminal
pnpm add @tessinaui/ui

Usage

example.tsx
import { Fab } from "@tessinaui/ui";
example.tsx
<Fab label="Create" leadingIcon={<Plus />} />

The FAB is the button family in a floating posture: same variants, same intents, same size ladder, same tone contract. What it adds is elevation, corner placement that clears the device's safe area, a glass fill for sitting over media, and two containers — FabGroup for a stack and FabMenu for a speed dial.

Rules worth following

  • One FAB per screen. It is the single most prominent action in a view; a second one competes with it rather than adding to it.
  • Never two floating buttons side by side. Put them in a FabGroup, which gives them one anchor and one gap.
  • Name an icon-only FAB. With no label, aria-label or aria-labelledby is required — the type will not compile without one.
  • Keep a speed dial to two to six actions, and never open one from an extended FAB.
  • Do not gate the FAB on content loading. Real apps render it enabled over skeleton lists and announce completion with a toast, rather than putting a spinner in the button.

Examples

Default

An extended FAB — a pill with a leading icon and a label.

Icon Only

Omit label for the plain FAB: the box goes square, the glyph steps up one size, and aria-label becomes the accessible name.

Variants

Four weights — primary, secondary, outline and ghost. The neutral primary is the quiet, high-contrast fill most product FABs use.

Intents

All twenty variant × intent combinations. Every one paints; intent is expressed through text colour on the quiet tiers and through the fill on primary.

Sizes

Six rungs. 2xs (24px) is square-only — it has no room for a label — and the sub-44px sizes grow an invisible tap extension on touch screens, tablets included, so they still meet the WCAG 2.5.5 touch target.

Rounded

The radius cascade. full is the default circle or pill; xl gives the squircle — softer than a square, still distinct from the round default.

Elevation

A floating control casts a shadow. raised is the default; overlay is for sitting over media. The shadow is dropped in dark mode by design — on a dark page there is nothing to darken, so separation comes from the fill's own contrast.

Placement

placement pins the FAB to a corner of the nearest positioned ancestor, insetting by max(1rem, env(safe-area-inset-*)) so it clears the home indicator and the notch. The safe-area half applies once your page sets viewport-fit=cover in its viewport meta tag; without it the browser keeps the page out of those areas itself and the inset is 1rem. It is absolute and opt-in, so an unplaced FAB stays in the normal flow.

Corner names are physical and do not mirror under dir="rtl" — ask for bottom-left if you want the trailing corner in an RTL layout.

Badge

A count for a cart or an inbox. The digits are aria-hidden, so put the count in the FAB's own accessible name — otherwise it is announced twice or not at all.

Group

FabGroup stacks related floating buttons under one anchor. The primary is written first, so it is the first tab stop, and paints at the bottom of the stack, nearest the thumb.

Map Controls

The same container as a named cluster of secondary controls, placed on the side edge rather than the corner — deliberately outside the thumb arc, because the bottom-right slot belongs to the screen's primary action.

Menu

The speed dial. Built on the house menu primitives, so arrow keys, typeahead, Escape and focus return to the trigger all work. The arrow keys follow the direction the actions fan out: in a dial that opens upward, the up arrow moves away from the button. The + rotates into a × when the menu opens.

The actions do not appear all at once: each is 40ms behind the one before it, so the stack unfurls outward from the button rather than snapping into place. Closing is a dismissal rather than a reveal, so everything leaves together. Only opacity and transform move, and the whole reveal is motion-safe: gated — under prefers-reduced-motion: reduce the items simply appear.

As a Link

render swaps the host element. An anchor host keeps link semantics: no role="button", activated by Enter only, and aria-disabled plus removal from the tab order rather than a disabled attribute an anchor would ignore.

States

Loading overlays its spinner rather than swapping it in, so the FAB keeps its width and its label; the control stays focusable and swallows activation instead of natively disabling itself.

On Color

Placed on a coloured Surface, FABs adopt the on-color tone automatically.

Skeleton

For a screen that is still composing. A FAB that is already mounted stays enabled over skeleton content — see the guidelines above.

RTL

Under dir="rtl" the icon slots mirror and the badge moves to the top-left corner. Unset, dir is inherited from the page. placement corners are physical — they do not mirror.

API Reference

Fab

PropTypeDefaultDescription
labelstring—Visible label (the extended FAB). Omit for the icon-only square FAB
variant"primary" | "secondary" | "outline" | "ghost""primary"Visual weight
intent"none" | "error" | "warning" | "success" | "info""none"Semantic meaning
tone"default" | "on-color"inheritedWhether it sits on a coloured surface
size"2xs" | "xs" | "sm" | "md" | "lg" | "xl""md"Density. 2xs is square-only
rounded"none" | "sm" | "md" | "lg" | "xl" | "full""full"Corner treatment
elevation"none" | "raised" | "overlay""raised"Drop shadow; dropped in dark mode by design
placement"none" | "bottom-right" | "bottom-center" | "bottom-left" | "top-right" | "top-left""none"Absolute corner placement, safe-area aware
translucentbooleanfalseGlass fill for use over media. Available, but not demonstrated on this page yet
leadingIconReact.ReactNode—Icon before the label. Decorative
trailingIconReact.ReactNode—Icon after the label. Needs a label
loadingbooleanfalseBusy. Overlays a spinner, keeps focus, swallows activation
disabledbooleanfalseNot operable. Wins over loading
showBadgebooleanfalseNotification dot on the top-trailing corner
badgeCountstring | number—Count inside the badge. aria-hidden — put it in the name too
renderReact.ReactElement—Render into another element, typically an <a>
dir"ltr" | "rtl"—Text direction. Unset, it inherits the page's direction
aria-labelstring—Required when there is no label

FabGroup

PropTypeDefaultDescription
orientation"vertical" | "horizontal""vertical"Stack direction
placementsame as Fab"none"The group owns the anchor so its children do not
aria-labelstring—Names the cluster; adds role="group" when present

FabMenu

FabMenu · FabMenuTrigger · FabMenuContent · FabMenuItem.

PropOnTypeDefaultDescription
closeIconTriggerReact.ReactNode—Icon while open. Omitted, the leading icon rotates 45°
sideContent"top" | "bottom" | "left" | "right""top"Which way the actions fan out
alignContent"start" | "center" | "end""end"Alignment against the trigger
sideOffsetContentnumber12Gap between trigger and first action
backdropContentbooleanfalseDim the page behind the open menu
dirContent"ltr" | "rtl"—Pass the trigger's dir. The menu portals to <body>, so align cannot mirror without it
iconItemReact.ReactNode—The action's glyph
labelItemstring—The action's name. Real text, so it is the accessible name
sizeItemFab["size"]"sm"Circle size

FabVisual

The FAB's pixels with none of its semantics, for the rare places a real button is illegal — inside a menuitem, for instance. If what you are building can be a button, use Fab.

Accessibility

  • An icon-only FAB requires aria-label or aria-labelledby; this is enforced in the props type, so the unnamed case does not compile.
  • loading reports aria-busy and aria-disabled and keeps the control focusable — a button that natively disables itself on click throws focus to the document body mid-interaction.
  • The focus ring is focus-visible:ring-2 against ring-offset-surface, so it is visible in both themes.
  • Touch targets meet 44×44px on any touch screen, phone or tablet: md and up are large enough already, and 2xs/xs/sm carry an invisible tap extension. Each speed-dial action carries a 44px band across its whole row. Note that a getBoundingClientRect() probe cannot see either — they are pseudo-elements.
  • Badge digits are aria-hidden; the count belongs in the FAB's accessible name.
  • FabMenu inherits the house menu's keyboard model: arrows, typeahead, Escape, and focus return to the trigger. The up and down arrows move in the direction the actions are painted, and the action nearest the button comes first whichever way the dial opens, including when it flips below the button for lack of room. Its items are named by their visible text rather than by a tooltip, which is what makes them work under touch. The action with keyboard focus wears the same focus ring as the FAB, on its circle.
  • Motion is motion-safe: gated, duration included, so prefers-reduced-motion: reduce silences it.

Screen reader coverage

ReaderStatus
VoiceOver (macOS)Passed — names, badge counts, and the loading busy state all announce
NVDAUntested — does not run on this machine
JAWSUntested — does not run on this machine
ToggleGroupCheckbox

On this page

When to useWhen not to useInstallationUsageRules worth followingExamplesDefaultIcon OnlyVariantsIntentsSizesRoundedElevationPlacementBadgeGroupMap ControlsMenuAs a LinkStatesOn ColorSkeletonRTLAPI ReferenceFabFabGroupFabMenuFabVisualAccessibilityScreen reader coverage