Frontend Development 10 min read

Deep Dive into a React Draggable Floating Ball Component

This article provides a comprehensive technical walkthrough of building an advanced draggable floating‑ball UI component in React, covering drag handling with react‑draggable, boundary calculations, edge‑snapping, circular menu creation via CSS transforms, right‑click context menus, and app‑switching logic, all illustrated with code snippets and visual examples.

Rare Earth Juejin Tech Community
Rare Earth Juejin Tech Community
Rare Earth Juejin Tech Community
Deep Dive into a React Draggable Floating Ball Component

Deep Dive into a React Draggable Floating Ball Component

In response to a request for a web‑based floating ball that also functions as a mac‑style menu bar, the author implemented a React component that combines both features, offering a draggable, edge‑snapping, circular menu with right‑click context actions.

Component Overview

The floating‑ball component provides the following features:

Draggable positioning

Circular application menu

Edge‑snapping (suction) behavior

Right‑click context menu

Quick application switching

Draggable Functionality

The react-draggable library is used to implement drag operations. The component listens to onStart , onDrag , and onStop events to manage drag state and position.

<Draggable
  onStart={(e, position) => {
    setDragging(true);
    setStartPosition(position);
  }}
  onDrag={(e, position) => {
    setPosition(position);
  }}
  onStop={(e, position) => {
    handleDragBoundary(e, position);
    setEndPosition(position);
    setDragging(false);
  }}
  handle="#centerButton"
  position={position}
>
  {/* floating ball content */}
</Draggable>

Drag movement is applied via transform: translate(x, y) , with the current coordinates stored in a state hook:

const [position, setPosition] = useState({ x: 0, y: 0 });

Boundary Calculation

The component first obtains the browser viewport size and the floating‑ball element dimensions:

const browserWidth = window.innerWidth;
const browserHeight = window.innerHeight;
const floatButtonNav = document?.getElementById('floatButtonNav');
if (!floatButtonNav) return;

const distanceLeft = floatButtonNav.getBoundingClientRect().left;
const floatButtonNavWidth = floatButtonNav.clientHeight || 64;
const floatButtonNavHeight = floatButtonNav.clientHeight || 64;

It then computes boundary limits (using a 120 px offset and a 10 px margin) to keep the ball within the visible area:

// 120 is the absolute positioning offset; 10 is the extra margin
const leftBoundary = -browserWidth + floatButtonNavWidth + 120 + 10;
const rightBoundary = 120 - 10;
const topBoundary = -browserHeight + floatButtonNavHeight + 120;
const bottomBoundary = 120;

During dragging, the position is clamped to these limits:

setPosition({
  x: x < leftBoundary ? leftBoundary : x > rightBoundary ? rightBoundary : x,
  y: y < topBoundary ? topBoundary : y > bottomBoundary ? bottomBoundary : y
});

Edge‑Snapping Effect

A suction state is introduced to snap the ball to the left or right edge when it crosses the corresponding boundary, triggering CSS animations for smooth transitions.

const handleSuction = (suction) => {
  onClose();
  setSuction(suction);
  setTimeout(() => {
    setLockSuction(false);
  }, 1000);
};

x < leftBoundary ? handleSuction(Suction.Left)
  : x > rightBoundary ? handleSuction(Suction.Right)
  : null;

The associated CSS defines the visual appearance and animation keyframes for left/right suction:

.floatBtn {
  position: absolute;
  width: 64px;
  height: 64px;
  border-radius: 50%;
  background: rgba(28,32,35,0.9);
  transition: all 0.5s ease;
  &[data-suction='left'] { transform: translateX(-100px); animation: slide-right-left 300ms cubic-bezier(...); }
  &[data-suction='right'] { transform: translateX(100px); animation: slide-left-right 300ms cubic-bezier(...); }
}
@keyframes slide-right-left { 0% { transform: translateX(40px); } 100% { transform: translateX(-100px); opacity: 0; } }
@keyframes slide-left-right { 0% { transform: translateX(-40px); } 100% { transform: translateX(100px); opacity: 0; } }

Circular Application Menu (Floating Ball)

The menu is built by converting rectangular elements into fan‑shaped slices, which together form a circular layout. Each slice represents a menu item, and a central small circle serves as the floating ball itself.

Implementation Principle

A circle is divided into 6‑8 equal fan slices; each slice corresponds to a menu item.

Angle per slice is calculated as 360 / numberOfItems .

Rectangles are rotated and skewed to become fan shapes.

Angle and skew values are memoized for performance:

const [degree, contentSkewDegree, contentRotateDegree] = useMemo(() => {
  const len = apps?.length < 6 ? 6 : apps?.length > 8 ? 8 : apps?.length;
  const temp = 360 / len;
  const skewDegree = -(90 - temp);
  const rotateDegree = -(90 - temp / 2);
  return [temp, skewDegree, rotateDegree];
}, [apps.length]);

The container applies both a skew (to cancel the parent fan’s skew) and a rotation (to face the centre):

transform={`skew(${contentSkewDegree}deg) rotate(${contentRotateDegree}deg)`}

Individual icons are rotated so they stay upright relative to the page:

const calculateDegree = (index) => {
  const temp = -(degree * index + contentRotateDegree);
  return `rotate(${temp}deg)`;
};

Right‑Click Context Menu

The react-contexify library provides a custom right‑click menu. The menu is displayed at a fixed offset relative to the floating ball:

const { show } = useContextMenu({ id: Floating_Button_Menu_Id });

const displayMenu = (e) => {
  e.stopPropagation();
  setIsRightClick(true);
  onClose();
  show({
    event: e,
    position: { x: '-65%', y: '-80%' }
  });
};

Application Switching

Clicking an app icon triggers logic to either open the home page, minimize the current app, or switch to the selected app:

const handleNavItem = (e, item) => {
  if (item.key === 'system-home') {
    // handle home logic
  } else if (item.pid === currentAppPid && item.size !== 'minimize') {
    // minimize current app
  } else {
    // switch to selected app
    switchAppById(item.pid);
  }
};

Conclusion

The article thoroughly dissects a sophisticated React floating‑ball component that integrates drag‑and‑drop, edge‑snapping, a circular fan‑menu, right‑click context actions, and efficient state management using React Hooks and useMemo . The provided code snippets and visual illustrations enable developers to reproduce and extend the component in their own projects.

reactUI Componentdraggablecontext menuCSS transformsedge snappingfloating ball
Rare Earth Juejin Tech Community
Written by

Rare Earth Juejin Tech Community

Juejin, a tech community that helps developers grow.

0 followers
Reader feedback

How this landed with the community

login Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.