A luxury watchmaker-inspired, highly customizable concentric rotary Floating Action Button (FAB) and precision Rotary Arc Dial / Slider for React.
- ⏱ Concentric Multi-Tier Orbital Arcs: Distribute 4, 9, or 20+ action items across balanced concentric arc tiers with smooth staggered entrance physics.
- 🧭 4-Corner & Edge Quadrant Math: Automatic trigonometric coordinate mapping for
bottom-left,bottom-right,top-left, andtop-right. - 🎛 6 Precision Dial Design Styles:
watchmaker(classic horology),minimal,cyber-segmented,neon-glow,retro-analog, andholographic. - 🎨 6 Built-in Themes:
luxury-watch(OLED obsidian & jewels),3d(tactile skeuomorphic push),glassmorphic(frosted crystal),cyberpunk(laser HUD),minimal-light(porcelain), andneon(ultra-violet). - 📳 Tactile Haptic Feedback Engine: Safe Web Vibration API engine with realistic tactile patterns (
tick,light,medium,boundary,danger,toggle). - 🌌 Studio Corner Ambient Backlighting: High-end radial, aurora, and neon corner ambient glow modes.
- 💅 Full CSS Variable Customization: Customize colors, glows, blur, backdrop, and sizes with CSS variables or Tailwind.
- 🛡 Zero Heavy UI Dependencies: Completely standalone — includes built-in accessible tooltips, badge counts, and notifications with zero bundle bloat.
- ♿ Full Accessibility: Keyboard arrow controls (
ArrowUp/Down/Left/Right,PageUp/Down,Home/End), mouse wheel support, and WCAG-compliant ARIA attributes. - 📦 100% TypeScript: Comprehensive type definitions and JSDoc annotations.
npm install react-rotary-fab
# or
yarn add react-rotary-fab
# or
pnpm add react-rotary-fabImport the stylesheet in your root entry file (App.tsx or index.tsx):
import 'react-rotary-fab/styles.css';import React, { useState } from 'react';
import { RotaryFab, Icons } from 'react-rotary-fab';
import 'react-rotary-fab/styles.css';
export default function App() {
const [volume, setVolume] = useState(70);
const [isPlaying, setIsPlaying] = useState(false);
const [isDialMode, setIsDialMode] = useState(false);
const items = [
// Tier 0: Quick Actions
{
id: 'vol',
title: 'Precision Volume Dial',
icon: <Icons.Volume />,
onClick: () => setIsDialMode(true)
},
{
id: 'play',
title: isPlaying ? 'Pause' : 'Play',
icon: isPlaying ? <Icons.Pause /> : <Icons.Play />,
active: isPlaying,
onClick: () => setIsPlaying(p => !p)
},
{
id: 'mic',
title: 'Microphone',
icon: <Icons.MicOn />,
danger: false
},
{
id: 'settings',
title: 'Settings',
icon: <Icons.Settings />,
hasBadge: true,
onClick: () => console.log('Opened settings')
}
];
return (
<div className="app-container">
<RotaryFab
items={items}
placement="bottom-left"
theme="luxury-watch"
enableHaptics={true}
dialMode={isDialMode}
onDialModeChange={setIsDialMode}
dialProps={{
value: volume,
min: 0,
max: 100,
step: 1,
unit: '% VOL',
onChange: setVolume
}}
/>
</div>
);
}You can also use the high-precision rotary arc slider anywhere as an independent component:
import React, { useState } from 'react';
import { RotaryDial } from 'react-rotary-fab';
export function BrightnessControl() {
const [brightness, setBrightness] = useState(80);
return (
<RotaryDial
value={brightness}
min={0}
max={100}
step={1}
radius={130}
placement="bottom-left"
unit="% LUX"
onChange={setBrightness}
showTicks={true}
showMicroDots={true}
showNeedle={true}
showGlow={true}
enableHaptics={true}
/>
);
}| Prop | Type | Default | Description |
|---|---|---|---|
items |
RotaryFabItem[] |
[] |
Array of action menu items distributed across concentric arcs. |
placement |
FabPlacement |
'bottom-left' |
Corner or edge placement ('bottom-left', 'bottom-right', 'top-left', 'top-right', 'bottom-center', 'top-center', 'center'). |
theme |
FabTheme |
'luxury-watch' |
Preset theme ('luxury-watch', 'glassmorphic', 'cyberpunk', 'minimal-light', 'neon'). |
animationMode |
AnimationMode |
'spring' |
Animation mode: 'spring', 'stagger', 'spiral', 'fan', 'scale', 'elastic', 'fade', 'none'. |
customTheme |
Record<string, string> |
undefined |
Custom CSS variable overrides. |
open |
boolean |
undefined |
Controlled open/expanded state. |
defaultOpen |
boolean |
false |
Uncontrolled default open state. |
onOpenChange |
(open: boolean) => void |
undefined |
Callback fired when open state changes. |
dialMode |
boolean |
undefined |
Controlled rotary dial/slider mode state. |
defaultDialMode |
boolean |
false |
Uncontrolled default dial mode state. |
onDialModeChange |
(isDial: boolean) => void |
undefined |
Callback fired when dial mode is toggled. |
dialProps |
Partial<RotaryDialProps> |
{} |
Options passed to the embedded rotary dial. |
arcConfigs |
ArcTierConfig[] |
[tier0, tier1, tier2] |
Custom radii and button sizes for concentric tiers. |
mainButtonSize |
number |
52 |
Diameter of the main floating action button in pixels. |
mainButton |
ReactNode | ((state) => ReactNode) |
undefined |
Custom render for the main FAB button. |
mainButtonIcon |
ReactNode |
<Icons.Menu /> |
Custom closed icon. |
mainButtonCloseIcon |
ReactNode |
<Icons.Close /> |
Custom open/expanded icon. |
mainButtonDialIcon |
ReactNode |
<Icons.Back /> |
Custom dial mode active icon. |
autoCollapseTimeout |
number | null |
9500 |
Inactivity timer (in ms) to auto-collapse the menu. Set to null to disable. |
closeOnItemClick |
boolean |
true |
Auto-close menu when an item is clicked. |
closeOnOutsideClick |
boolean |
true |
Auto-close when clicking outside. |
enableHaptics |
boolean |
true |
Enable safe tactile vibration on supported devices. |
showOrbitLines |
boolean |
true |
Enable or disable concentric dashed SVG orbit guidelines. |
showCornerBackdrop |
boolean |
true |
Show ambient radial gradient corner backdrop. |
renderItem |
(item, index, tier) => ReactNode |
undefined |
Custom item renderer function. |
interface RotaryFabItem {
id: string; // Unique identifier
title?: string; // Tooltip and accessible title
icon?: React.ReactNode; // Icon element
children?: React.ReactNode; // Custom button inner content
onClick?: (event: React.MouseEvent) => void; // Click handler
active?: boolean; // Active / highlighted state
danger?: boolean; // Danger / destructive action styling
disabled?: boolean; // Disabled state
hasBadge?: boolean | string | number; // Indicator dot or numerical count badge
badgeColor?: string; // Custom badge color
tooltipPlacement?: 'top' | 'bottom' | 'left' | 'right' | 'auto'; // Tooltip direction
ariaLabel?: string; // Accessible ARIA label
className?: string; // Custom CSS class
style?: React.CSSProperties; // Custom inline styles
tier?: number; // Explicit tier override (0 = innermost)
haptic?: HapticType; // Custom haptic pattern on click
}| Prop | Type | Default | Description |
|---|---|---|---|
value |
number |
undefined |
Controlled value. |
defaultValue |
number |
50 |
Uncontrolled default value. |
min |
number |
0 |
Minimum value. |
max |
number |
100 |
Maximum value. |
step |
number |
1 |
Increment step. |
radius |
number |
124 |
Radius of the dial arc in pixels. |
placement |
FabPlacement |
'bottom-left' |
Corner placement. |
unit |
string |
'% VOL' |
Unit suffix displayed in the chrono readout badge. |
label |
string | ((val) => ReactNode) |
undefined |
Custom readout formatter. |
showTicks |
boolean |
true |
Render precision watch tick marks. |
tickCount |
number |
20 |
Number of discrete tick marks. |
showMicroDots |
boolean |
true |
Render inner celestial micro-dot orbit. |
showNeedle |
boolean |
true |
Render watch dial needle on thumb. |
showGlow |
boolean |
true |
Enable glowing drop-shadow filters. |
enableHaptics |
boolean |
true |
Enable physical step haptics while dragging. |
onChange |
(value: number) => void |
undefined |
Callback fired on continuous dragging/scrubbing. |
onChangeEnd |
(value: number) => void |
undefined |
Callback fired when user releases pointer. |
You can customize every aspect of the FAB using CSS variables:
:root {
--rf-bg-main: linear-gradient(135deg, #18191c 0%, #0d0e11 100%);
--rf-bg-item: rgba(24, 25, 29, 0.88);
--rf-bg-item-hover: rgba(42, 44, 52, 0.96);
--rf-bg-item-active: #ffffff;
--rf-border: rgba(255, 255, 255, 0.18);
--rf-text: #ffffff;
--rf-text-muted: rgba(255, 255, 255, 0.55);
--rf-accent: #38bdf8;
--rf-accent-glow: rgba(56, 189, 248, 0.75);
--rf-danger: #ef4444;
--rf-danger-glow: rgba(239, 68, 68, 0.7);
--rf-track: rgba(255, 255, 255, 0.15);
--rf-fill: #38bdf8;
--rf-shadow: 0 8px 32px rgba(0, 0, 0, 0.65);
--rf-tooltip-bg: #0f1013;
--rf-tooltip-text: #ffffff;
}Or pass customTheme directly to the component:
<RotaryFab
items={items}
customTheme={{
'--rf-accent': '#10b981',
'--rf-accent-glow': 'rgba(16, 185, 129, 0.8)'
}}
/>| Input | Action |
|---|---|
Pointer / Touch Drag |
Smooth circular scrubbing with physical step snapping |
ArrowUp / ArrowRight |
Increment value by step |
ArrowDown / ArrowLeft |
Decrement value by step |
PageUp / PageDown |
Increment / decrement by large step (5 * step) |
Home / End |
Snap directly to min / max |
Mouse Wheel |
Increment / decrement when hovering dial |
Space / Enter |
Toggle button activation |
![]() Nikhil Eashy 💻 Code, Architecture & Development |
![]() Neeha Nazer 🎨 Design, Aesthetics & Concept Contribution |
Contributions are welcome! Please check out CONTRIBUTING.md for details on setting up the development environment, running tests, and opening pull requests.
MIT © Nikhil Eashy

