Floating FAB Gagan
Single floating action button at bottom-center of screen. Ink background with cream plus glyph. Press state scale(0.97). Shadow-float for elevation. The primary entry point to add a new expense — always visible, never ambiguous about what it does.
Default
screen content
The FAB sits at bottom-center, vertically offset above the tab bar. The mock shows the positional relationship — the FAB overlaps the tab bar's center slot, which should be left empty or act as a visual placeholder.
Bottom offset: 64px (tab bar height) + 16px (gap) = 80px from screen bottom. On devices with a home indicator (iPhone X+), add safe area inset: calc(80px + env(safe-area-inset-bottom)).
States
Default
Hover
Pressed
Disabled
Usage
Do
Use exactly one FAB per screen. The FAB is the primary action — "add expense." It should appear on every screen where adding an expense is relevant (home, group detail, expense list). Hide it on screens where the action doesn't apply (settings, profile).
Don't
Don't use FAB for secondary or contextual actions. If the action changes based on context (e.g. "add member" on a group screen vs "add expense" on home), that's a signal to rethink the IA, not a reason to change the FAB's icon dynamically.
Do
Account for the tab bar in vertical positioning. Bottom offset must always be
tab-bar-height + gap, plus safe area inset on notched devices. Test on iPhone SE (smallest) and iPhone 15 Pro Max (largest + dynamic island).
Don't
Don't animate from
scale(0) on mount. The entrance uses translateY(12px) + scale(0.92) with ease-spring. Don't use transition: all — enumerate transform and box-shadow separately.
Spec
| Property | Value | Notes |
|---|---|---|
| Size | 56 × 56px | Circle, border-radius 999px |
| Background | --ink | #171513 |
| Glyph | + (fullwidth plus) | 26px, font-weight 500 |
| Glyph color | --s-paper | #FBF8F0 |
| Shadow | --shadow-float | Resting elevation |
| Hover shadow | --shadow-lg | Pointer devices only |
| Press shadow | --shadow-md | Active state |
| Hover scale | 1.04 | Pointer devices only via @media (hover: hover) |
| Press scale | 0.97 | :active, 140ms ease-out |
| Position | fixed, bottom-center | Above tab bar + safe area inset |
| Z-index | --z-nav (50) | Above content, below modals/sheets |
| Entrance duration | 240ms | ease-spring, from translateY(12px) + scale(0.92) |
Accessibility
Label
The FAB shows only a plus glyph. Always include
aria-label="Add expense" on the button element. Never leave it unlabeled — screen reader users must know this is the primary action.
Touch target
56px meets the 44×44pt minimum touch target. On dense UIs where surrounding content might be tappable, add
padding: 8px to the wrapping element to extend the tap zone without changing visual size.
Focus ring
Add a visible focus ring for keyboard users:
:focus-visible { outline: 2px solid var(--teal); outline-offset: 3px; }. Never suppress focus with just outline: none.
Code
HTML
<div class="fab"> <button class="fab-btn" aria-label="Add expense">+</button> </div>
CSS — safe area positioning
.fab { position: fixed; bottom: calc(64px + 16px + env(safe-area-inset-bottom)); left: 50%; transform: translateX(-50%); z-index: var(--z-nav); }
Design tokens used
| Token | Value | Role |
|---|---|---|
--ink | #171513 | Button background |
--s-paper | #FBF8F0 | Plus glyph color |
--shadow-float | — | Resting elevation |
--shadow-lg | — | Hover elevation |
--shadow-md | — | Pressed elevation |
--d-fast | 140ms | Press transition duration |
--d-med | 240ms | Entrance animation duration |
--ease-out | cubic-bezier(0.23,1,0.32,1) | Press easing |
--ease-spring | cubic-bezier(0.34,1.56,0.64,1) | Entrance easing |
--z-nav | 50 | Z-index layer |
See it in context
Interactive prototype — this component is live below. Tap, swipe, and drag to explore.
The floating FAB appears in
buttons action mode (the default). It is the teal + button at the bottom-centre of the group screen. Tap to see the press state.