Loading…
Loading…
A circular button that floats above the UI to promote a primary action.
The Floating Action Button (FAB) is a circular button that hovers above the interface to promote the single most important action on a screen. Popularized by Material Design, the FAB occupies a fixed position (typically bottom-right) and uses elevation, color, and size to stand out from all other elements.
The FAB represents a design philosophy: every screen should have a clear primary action, and that action should be immediately accessible regardless of scroll position. A compose button in an email app, a create button in a project tool, or an add-to-cart button in a product detail page — these are classic FAB use cases.
When to use a FAB:
When NOT to use a FAB:
The FAB's visual prominence relies on elevation. Use the Shadow Generator to craft the right box-shadow — typically --shadow-lg or higher. The FAB's circular shape means border-radius is always 50% or large enough to create a circle, which you can preview with the Border Radius Generator. Ensure the FAB icon or label contrasts against its background with the Contrast Checker. For the entrance animation, explore easing curves with the Animation Generator.
| Variant | Size | Content | Use Case |
|---|---|---|---|
| Standard FAB | 56px diameter | Icon only | Default primary action button |
| Mini FAB | 40px diameter | Icon only | Secondary floating action, or when standard FAB is too visually heavy |
| Extended FAB | Auto width, 48–56px height | Icon + label text | When the action needs textual clarification ("New message", "Create project") |
| Large FAB | 96px diameter | Large icon (36px) | Landing pages, hero sections, rare emphasis situations |
| Position | CSS | Use Case |
|---|---|---|
| Bottom-right | bottom: 16px; right: 16px; | Default position. Most common. Right-hand thumb reach on mobile. |
| Bottom-center | bottom: 16px; left: 50%; transform: translateX(-50%); | Centered layouts, music players, media controls |
| Bottom-left | bottom: 16px; left: 16px; | RTL layouts, or when bottom-right is occupied |
| Top-right | top: 16px; right: 16px; | Rarely used. Conflicts with navigation and close buttons. |
The Speed Dial pattern transforms the FAB into a launcher for 3–6 related actions:
| State | Behavior |
|---|---|
| Collapsed | Single FAB with primary icon (e.g., +) |
| Expanded | FAB icon rotates to ×, mini-FABs fan out vertically above with labels |
| Action selected | Speed dial collapses, selected action executes |
Speed dial actions should be labeled — a row of unlabeled icon circles is confusing. Each mini-FAB should have a text label positioned to the side.
| Property | Type | Default | Description |
|---|---|---|---|
variant | 'standard' | 'mini' | 'extended' | 'large' | 'standard' | Size and shape variant |
color | 'primary' | 'secondary' | 'surface' | 'primary' | Background color theme |
icon | ReactNode | — | Icon element (required for all variants) |
label | string | — | Text label (required for extended variant, serves as aria-label for icon-only) |
position | 'bottom-right' | 'bottom-center' | 'bottom-left' | 'bottom-right' | Fixed screen position |
offset | { bottom?: number; right?: number; left?: number } | { bottom: 16, right: 16 } | Position offset in pixels |
visible | boolean | true | Controls visibility with entrance/exit animation |
disabled | boolean | false | Disables interaction |
onClick | () => void | — | Click handler |
externalWindow | boolean | false | When true, adjusts position to account for bottom navigation bars |
as | ElementType | 'button' | Polymorphic element override |
Important: Every FAB MUST have an accessible label. For icon-only FABs, provide aria-label. For extended FABs, the visible label text serves as the accessible name. The Button component page covers additional button prop patterns that apply to FABs.
| Token Category | Token Example | FAB Usage |
|---|---|---|
| Color – Fill | --color-primary-500 | Standard FAB background. Use a vibrant, high-saturation value. |
| Color – Text/Icon | --color-on-primary | Icon and label color on filled FAB |
| Color – Surface | --color-surface-elevated | Surface-colored FAB variant |
| Shadow – Resting | --shadow-lg | Resting elevation — FABs need prominent shadow. Design with Shadow Generator. |
| Shadow – Hover | --shadow-xl | Elevated shadow on hover — shadow grows to indicate interactivity |
| Shadow – Active | --shadow-md | Reduced shadow on press — "pushes down" into the surface |
| Border – Radius | 50% or --radius-full | Always fully circular for standard/mini/large. Extended uses pill radius. Preview with Border Radius Generator. |
| Sizing – Standard | 56px | Standard FAB diameter |
| Sizing – Mini | 40px | Mini FAB diameter |
| Sizing – Large | 96px | Large FAB diameter |
| Sizing – Extended Height | 48px – 56px | Extended FAB height |
| Spacing – Padding | --space-4 (16px) | Horizontal padding for extended FAB label area |
| Spacing – Gap | --space-2 (8px) | Gap between icon and label in extended variant |
| Transition – Transform | --duration-normal (200ms), cubic-bezier(0.4, 0, 0.2, 1) | Entrance/exit scale animation. Tune with Animation Generator. |
| Z-Index | --z-fab (typically 1050–1100) | Above content, below modals and drawers |
| State | Visual Treatment | Notes |
|---|---|---|
| Default | Brand color fill, icon centered, large shadow | The resting floating state |
| Hover | Shadow grows (shadow-lg → shadow-xl), optional slight scale (1.05) | Elevation change communicates interactivity |
| Active/Pressed | Shadow shrinks (shadow-lg → shadow-md), slight scale-down (0.95) | "Press into surface" tactile feedback |
| Focused | Focus ring — offset from the circular boundary to avoid clipping | Must be visible for keyboard users. Use outline-offset: 3px. |
| Disabled | Muted colors, no shadow, cursor: not-allowed | FABs should rarely be disabled — prefer hiding or showing a tooltip explaining why |
| Loading | Spinner replaces icon, interaction blocked | Show a circular spinner matching the FAB's size |
| Entering | Scale from 0 → 1 with ease-out curve | Animate in with Animation Generator curves. Respect prefers-reduced-motion. |
| Exiting | Scale from 1 → 0 with ease-in curve | Animate out before DOM removal |
| Behavior | Implementation | Use Case |
|---|---|---|
| Always visible | Fixed position, no scroll logic | Default — FAB stays visible always |
| Hide on scroll down | Translate off-screen when scrollDirection === 'down' | Reduces visual clutter during content consumption |
| Show on scroll up | Translate back when scrollDirection === 'up' | Re-appears when user wants to act |
| Transform to extended | Collapse extended → standard on scroll, expand on stop | Saves space while scrolling, shows label when idle |
FABs introduce unique accessibility challenges due to their floating, fixed position and reliance on icons.
Labeling (WCAG 4.1.2 – Name, Role, Value):
aria-label providing a clear action description: aria-label="Compose new message", not aria-label="Plus"aria-label needed unless the label is ambiguousaria-label values describing their specific actionKeyboard Access (WCAG 2.1.1 – Keyboard):
tabindex values above 0. Place the FAB element after the main content in DOM order so it appears at the end of the tab sequence.Focus Visibility (WCAG 2.4.7 – Focus Visible):
outline-offset to prevent clipping by border-radius: 50% combined with overflow: hiddenColor Contrast (WCAG 1.4.3 – Contrast Minimum):
Motion (WCAG 2.3.3 – Animation from Interactions):
prefers-reduced-motion: reduceOverlap Concerns:
padding-bottom: 80px) to scrollable containersDo:
Don't:
<!-- Standard FAB -->
<button class="fab" aria-label="Create new item">
<svg class="fab-icon" aria-hidden="true" viewBox="0 0 24 24">
<path d="M12 5v14M5 12h14" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
</svg>
</button>
<!-- Extended FAB -->
<button class="fab fab--extended">
<svg class="fab-icon" aria-hidden="true" viewBox="0 0 24 24">
<path d="M12 5v14M5 12h14" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
</svg>
<span class="fab-label">New message</span>
</button>
<!-- Mini FAB -->
<button class="fab fab--mini" aria-label="Add attachment">
<svg class="fab-icon" aria-hidden="true" viewBox="0 0 24 24">
<path d="M12 5v14M5 12h14" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
</svg>
</button>
<style>
.fab {
position: fixed;
bottom: 16px;
right: 16px;
width: 56px;
height: 56px;
border-radius: 50%;
border: none;
background: var(--color-primary-500);
color: var(--color-on-primary);
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
box-shadow: 0 6px 10px rgba(0,0,0,0.14),
0 1px 18px rgba(0,0,0,0.12),
0 3px 5px rgba(0,0,0,0.2);
transition: box-shadow 0.2s ease, transform 0.2s ease;
z-index: 1050;
}
.fab:hover {
box-shadow: 0 8px 15px rgba(0,0,0,0.18),
0 3px 24px rgba(0,0,0,0.15),
0 4px 8px rgba(0,0,0,0.22);
transform: scale(1.05);
}
.fab:active {
box-shadow: 0 4px 6px rgba(0,0,0,0.12),
0 1px 10px rgba(0,0,0,0.1);
transform: scale(0.95);
}
.fab:focus-visible {
outline: 2px solid var(--color-primary-300);
outline-offset: 3px;
}
.fab-icon {
width: 24px;
height: 24px;
}
.fab--mini {
width: 40px;
height: 40px;
}
.fab--mini .fab-icon {
width: 20px;
height: 20px;
}
.fab--extended {
width: auto;
border-radius: 28px;
padding: 0 20px;
gap: 8px;
}
.fab-label {
font-size: 0.875rem;
font-weight: 500;
letter-spacing: 0.02em;
white-space: nowrap;
}
@media (prefers-reduced-motion: reduce) {
.fab {
transition: none;
}
}
</style>import React, { useState, useEffect } from 'react';
import styles from './FAB.module.css';
import clsx from 'clsx';
interface FABProps {
variant?: 'standard' | 'mini' | 'extended' | 'large';
color?: 'primary' | 'secondary' | 'surface';
icon: React.ReactNode;
label?: string;
position?: 'bottom-right' | 'bottom-center' | 'bottom-left';
offset?: { bottom?: number; right?: number; left?: number };
visible?: boolean;
disabled?: boolean;
onClick?: () => void;
'aria-label'?: string;
}
export function FAB({
variant = 'standard',
color = 'primary',
icon,
label,
position = 'bottom-right',
offset = { bottom: 16, right: 16 },
visible = true,
disabled = false,
onClick,
'aria-label': ariaLabel,
}: FABProps) {
const [scrollVisible, setScrollVisible] = useState(true);
const isExtended = variant === 'extended';
useEffect(() => {
let lastScrollY = window.scrollY;
const handleScroll = () => {
const currentY = window.scrollY;
setScrollVisible(currentY < lastScrollY || currentY < 100);
lastScrollY = currentY;
};
window.addEventListener('scroll', handleScroll, { passive: true });
return () => window.removeEventListener('scroll', handleScroll);
}, []);
const isVisible = visible && scrollVisible;
const positionStyles: React.CSSProperties = {
position: 'fixed',
bottom: offset.bottom,
...(position === 'bottom-right' && { right: offset.right }),
...(position === 'bottom-left' && { left: offset.left }),
...(position === 'bottom-center' && {
left: '50%',
transform: 'translateX(-50%)',
}),
};
return (
<button
className={clsx(styles.fab, styles[variant], styles[color], {
[styles.hidden]: !isVisible,
})}
style={positionStyles}
onClick={onClick}
disabled={disabled}
aria-label={isExtended ? undefined : (ariaLabel || label)}
>
<span className={styles.icon} aria-hidden="true">{icon}</span>
{isExtended && label && (
<span className={styles.label}>{label}</span>
)}
</button>
);
}Material Design (MUI) is the origin of the FAB pattern. MUI provides <Fab> with size (small, medium, large), color (default, primary, secondary, inherit), and variant (circular, extended). The <SpeedDial> component implements the expandable pattern with <SpeedDialAction> children. MUI handles elevation transitions (resting: 6dp, pressed: 12dp), the circular ripple effect, and prefers-reduced-motion. The FAB respects the theme's shape.borderRadius for extended variant and uses palette.primary for the fill. MUI recommends one FAB per screen and provides the sx prop for position overrides. Use the Shadow Generator to customize MUI's elevation values.
Ant Design does not include a FAB component in its core library historically. The pattern is less common in enterprise/B2B applications (Ant's primary audience). Teams needing a FAB in Ant Design projects typically create a custom component using Ant's <Button type="primary" shape="circle"> with fixed positioning and custom shadows. The <FloatButton> component was added in Ant Design v5, supporting type (primary, default), shape (circle, square), tooltip, and a <FloatButton.Group> for speed-dial-like expansion.
Chakra UI does not include a dedicated FAB. Developers compose one using <IconButton isRound> with position="fixed" and Chakra's shadow tokens (shadow="lg"). Chakra's useDisclosure hook handles speed dial open/close state. The animation can be handled via Chakra's <ScaleFade> or Framer Motion integration.
Bootstrap has no FAB component. The pattern can be built with Bootstrap's .btn .btn-primary .rounded-circle classes, position-fixed, and utility classes for bottom/end positioning. Bootstrap's .shadow-lg class provides the elevation. This is a purely custom implementation with no JavaScript support from Bootstrap.
Apple Human Interface Guidelines does not endorse the FAB pattern. Apple's design language uses navigation bars, toolbars, and tab bars for primary actions. The closest equivalent is the compose button in Mail (a bottom-right floating circle on iPad/iPhone), but Apple treats this as a specific app-level decision, not a general-purpose component. In SwiftUI, there is no FAB view — developers use a Button with .clipShape(Circle()) and manual overlay positioning.
Tailwind CSS FABs are built with utility classes: fixed bottom-4 right-4 w-14 h-14 rounded-full bg-indigo-600 text-white shadow-lg hover:shadow-xl active:shadow-md transition-all flex items-center justify-center. For the entrance animation, animate-scale-up can be defined in tailwind.config.js. Headless UI and Radix provide no FAB primitive — it's considered an application-level pattern rather than a design system primitive. Configure entrance timing with the Animation Generator.