Radial FAB Gagan
Long-press floating action button that fans four options in an arc above the FAB center. Backdrop blurs and darkens while the menu is open. Each option has a scale-up hot state when the pointer enters. Tap (no hold) triggers quick-add.
States
Closed — tap to quick-add
Resting state. A single 56px ink button with a float shadow. Tapping (not holding) opens quick-add directly — the most common action gets the shortest path.
Open — long-press reveals arc
+
Add
⇌
Settle
⊡
Scan
✎
Note
After 320ms press: backdrop blurs at 4px, four items fan out on an arc at −150°, −110°, −70°, −30°. The FAB icon rotates or switches to ×. Items scale up on hover. Pressing Escape or tapping outside closes the menu.
Spec
| Property | Value | Notes |
|---|---|---|
| Long-press threshold | 320ms | Below this, treat as a tap (quick-add) |
| Item count | 4 | Add, Settle, Scan, Note |
| Arc angles | −150° −110° −70° −30° | Measured from positive x-axis; arc opens upward |
| Arc distance | 100–116px | Center of FAB to center of item icon |
| Item icon size | 44×44px | 14px border-radius (var(--r-md) approximation) |
| Hit area | 34px radius per item | Extends beyond visible icon for reliable touch targeting |
| Backdrop blur | 4px | backdrop-filter: blur(4px) |
| Backdrop tint | rgba(23,21,19, 0.30) | Warm tint — not gray |
| Open animation | 240ms | Items stagger 30ms apart, var(--ease-spring) |
| Tap (no hold) | Quick-add | Opens the add-expense sheet directly |
| Escape key | Closes menu | Also: tap backdrop, tap FAB again |
Anatomy
| Part | Element | Notes |
|---|---|---|
| FAB | .rfab-trigger | 56px ink circle, float shadow. Single child of the fixed position container. |
| Backdrop | .rfab-backdrop | Fixed inset-0, pointer-events captured when open. Opacity-animated. |
| Menu | .rfab-menu | Absolutely positioned relative to FAB center. Contains item slots. |
| Item | .rfab-item | Icon + label pair. Position computed via angle + distance from FAB center. |
| Item icon | .rfab-item-icon | 44px paper square, shadow-md. Scales to 1.12 on hover. |
| Item label | .rfab-item-label | Mono 9px uppercase. White on the darkened backdrop. |
Usage
Do
Keep the FAB fixed to the bottom-center of the screen, above the tab bar. It must clear the safe area inset on iOS. The 4-item arc opens upward — never sideways, never downward into the tab bar.
Don't
Don't exceed 4 items in the arc. Beyond 4, the angular spacing becomes too tight and items overlap on small screens. If more actions are needed, revisit the IA rather than adding more slots.
Do
Animate each item with a small stagger (30ms between siblings) so the arc fans out rather than popping all at once. Use
var(--ease-spring) for the scale-in — it gives the items a physical quality.
Don't
Don't use
transition: all on items or the FAB. Enumerate properties: transform, opacity. Animating all can trigger unexpected layout recalculations during the backdrop blur.
Accessibility
Keyboard
Long-press semantics don't map to keyboard. Provide a keyboard alternative: focus the FAB, press Space or Enter to open the menu (same as tap, no hold required). Tab cycles through the 4 items. Escape closes. Focus returns to the FAB on close.
Screen readers
The FAB should have
aria-label="Add expense" when closed. When open, update to aria-label="Close action menu" and use aria-expanded="true". Each item needs an aria-label — the icon alone is not sufficient. The backdrop should be aria-hidden="true".
Code
HTML structure
<div class="rfab-container"> <!-- Backdrop (hidden by default) --> <div class="rfab-backdrop" aria-hidden="true"></div> <!-- Arc menu (hidden by default) --> <div class="rfab-menu" role="menu" aria-label="Quick actions"> <button class="rfab-item" role="menuitem" aria-label="Add expense"> <div class="rfab-item-icon">+</div> <span class="rfab-item-label">Add</span> </button> <!-- ... more items --> </div> <!-- Trigger --> <button class="rfab-trigger" aria-label="Add expense" aria-expanded="false" aria-haspopup="menu" >+</button> </div>
Item position formula
// Compute x/y offsets for each arc item // angle in degrees, measured from positive x-axis (0° = right) function itemOffset(angleDeg, distance = 108) { const rad = (angleDeg * Math.PI) / 180; return { x: Math.cos(rad) * distance, // positive = right y: -Math.sin(rad) * distance, // negative = up (CSS y flipped) }; } // Items: -150°, -110°, -70°, -30° const angles = [-150, -110, -70, -30]; const items = ['add', 'settle', 'scan', 'note'];
Design tokens used
| Token | Role |
|---|---|
--ink | FAB background |
--s-paper | FAB icon color; item icon background |
--shadow-float | FAB elevation |
--shadow-md | Item icon elevation |
--ease-spring | Item open animation |
--d-med | Backdrop fade duration (240ms) |
See it in context
Interactive prototype — this component is live below. Tap, swipe, and drag to explore.
Switch the tweaks panel action mode to
radial. Long-press (320ms) the FAB to fan out 4 action options on an arc. Move your finger to select, release to commit.