DocumentationComponentsTheme CreatorGitHub
Theme CreatorGitHubIntroduction
InstallationUsageTheming
ComponentsAccordionAction SheetAlertAlertDialogArea ChartAspectRatioAvatarBadgeBannerBar ChartBottom NavBreadcrumbButtonButtonGroupCalendarCardCarouselChartChatBubbleChatBubbleNewCheckboxChipCoachMarkCodeBlockCollapsibleColor PickerComboboxCommandContainerContextMenuDate PickerDividerDrawerDropdown MenuEmptyStateFABFieldFieldsetFile UploadFlexFormGridHoverCardIconButtonLabelLine ChartLinkMenubarMeterModalNavigation MenuNumberFieldOTP InputPaginationPickerPie ChartPopoverProgressPromptInputRadar ChartRadial ChartRadioRatingScroll AreaSearchSegmentedControlSelectShortcutSidebarSkeletonSliderCircularSliderMediaTrimmerSpacerSpinnerSplit ButtonStackStatusStepperSurfaceSwitchTableTabsTextareaTime PickerToastToggleButtonToggleGroupTokenizerToolbarTooltipTop Header DesktopTop Header Mobile
Contributing
Components

FAB

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

Playground

Installation

pnpm add @tessinaui/ui

Usage

import { Fab } from "@tessinaui/ui";
<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 phones 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 Material specifies for its own FABs.

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. 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 + 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.

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""ltr"Text 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
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 phones: md and up are large enough already, and 2xs/xs/sm carry an invisible tap extension. Note that a getBoundingClientRect() probe cannot see it — it is a pseudo-element.
  • 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. Its items are named by their visible text rather than by a tooltip, which is what makes them work under touch.
  • 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
EmptyStateField

On this page

PlaygroundInstallationUsageRules worth followingExamplesDefaultIcon OnlyVariantsIntentsSizesRoundedElevationPlacementBadgeGroupMap ControlsMenuAs a LinkStatesOn ColorAPI ReferenceFabFabGroupFabMenuFabVisualAccessibilityScreen reader coverage