# How the Floating Ball UI Works in FluentRead: Vue 3 Component Architecture

> Explore how the floating ball UI works in FluentRead. Learn about its Vue 3 component architecture, drag-and-drop features, and event system for seamless translation.

- Repository: [ThinkStu/fluentread](https://github.com/bistutu/fluentread)
- Tags: architecture
- Published: 2026-02-26

---

**The floating ball UI in FluentRead is a self-contained Vue 3 component that renders a draggable, animated trigger button, manages drag-and-drop positioning with viewport clamping, and dispatches custom DOM events to bridge user interactions with the extension's translation engine.**

The floating ball UI serves as the primary interaction surface for FluentRead, an open-source browser extension that provides instant page translation. This Vue 3-based component appears as an always-on-top circular button that users can drag to either side of the viewport or click to toggle translation modes.

## Architecture Overview of the Floating Ball UI

The implementation follows a clean separation between presentation and logic. The UI layer lives in [`components/FloatingBall.vue`](https://github.com/bistutu/fluentread/blob/main/components/FloatingBall.vue), while the integration and lifecycle management reside in [`entrypoints/utils/floatingBall.ts`](https://github.com/bistutu/fluentread/blob/main/entrypoints/utils/floatingBall.ts). This decoupling allows the component to remain framework-agnostic in its event dispatching while the utility handles browser-specific concerns like storage persistence and translation engine coordination.

## Core Component: FloatingBall.vue

The [`FloatingBall.vue`](https://github.com/bistutu/fluentread/blob/main/FloatingBall.vue) file contains the single-file component that renders the interactive orb. It manages visual states, drag physics, and event communication through Vue 3's Composition API.

### Rendering and Visual States

The template uses dynamic class bindings to drive CSS animations. Key reactive refs include `isExpanded`, `isDragging`, `isTranslating`, and `isAnimating`, which toggle classes like `floating-ball-expanded`, `dragging`, and `animating`. The root element receives a `data-position` attribute that CSS uses to anchor the ball to the left or right viewport edge.

Props declared in the `<script setup>` block include:
- `position`: Initial side placement (`'left'` or `'right'`)
- `showMenu`: Controls visibility of auxiliary menu items
- `iconType`: Determines which iconography to render
- `showShortcutTooltip` and `shortcutTip`: Configure the keyboard shortcut hint

### Drag and Drop Positioning

The component implements viewport-constrained dragging through three core methods: `startDrag`, `drag`, and `stopDrag`. The logic distinguishes between clicks and drags by measuring time and distance—if the mouse-down lasts longer than 200 milliseconds or moves more than 5 pixels, the interaction is classified as a drag.

During the `drag` phase, the component calculates `draggedY` relative to the viewport height and updates `positionStyle` to apply a CSS transform. `stopDrag` clamps the final position, determines whether the ball should snap to the left or right edge based on the final X coordinate, and persists this choice in `internalPosition`. It then emits `onPositionChanged` to notify the parent utility.

### Hover Expansion and User Interaction

The `expandBall` and `collapseBall` methods manage the hover state. When the mouse enters the ball element, `isExpanded` flips to true, triggering the `floating-ball-expanded` class that scales the orb and adds a shadow. This behavior is suppressed while `isDragging` is true to prevent visual glitches during drag operations.

### Translation Toggle and Event Dispatching

Clicking the ball invokes `toggleTranslation`, which guards against misinterpreting drag-end events as clicks. The method flips the `isTranslating` boolean, triggers the visual animation via `triggerAnimation`, and dispatches two custom DOM events: `fluentread-translation-started` or `fluentread-translation-ended` depending on the new state.

These events serve as the bridge to the translation engine. The component also listens for the external event `fluentread-toggle-translation` via `handleExternalToggle`, allowing hot-key handlers or background scripts to programmatically control the ball.

### Animation and Ripple Effects

The `triggerAnimation` method sets `isAnimating` to true, which activates CSS keyframes defined in the stylesheet (such as `pulse-green` and `pulse-blue`). Simultaneously, `addRippleEffect` injects a temporary `<div class="ripple">` element into the DOM, applies a scaling animation, and removes it after the transition completes. This provides immediate visual feedback that the translation action has been registered.

## Integration Layer: floatingBall.ts

While the Vue component handles presentation, [`entrypoints/utils/floatingBall.ts`](https://github.com/bistutu/fluentread/blob/main/entrypoints/utils/floatingBall.ts) manages the floating ball's lifecycle and its connection to the broader extension architecture.

### Mounting and Lifecycle Management

The `mountFloatingBall()` function creates a container `div` with the ID `fluent-read-floating-ball-container`, initializes the Vue application with the `FloatingBall` component, and mounts it into the page DOM. It wires the `onPositionChanged` callback to persist the ball's side (left or right) and the `onTranslationToggle` callback to trigger the actual translation logic.

The utility also exposes `unmountFloatingBall()` for clean removal, ensuring all event listeners and Vue app instances are properly destroyed to prevent memory leaks.

### Bridging UI Events to Translation Logic

When the user clicks the ball, the component emits a custom event that [`floatingBall.ts`](https://github.com/bistutu/fluentread/blob/main/floatingBall.ts) captures. The utility then invokes `autoTranslateEnglishPage()` from [`entrypoints/main/trans.ts`](https://github.com/bistutu/fluentread/blob/main/entrypoints/main/trans.ts) to begin translation, or `restoreOriginalContent()` to revert the page. It dispatches the corresponding `fluentread-translation-started` and `fluentread-translation-ended` events so that other extension components can react to state changes.

The `toggleFloatingBallTranslation()` and `handleFloatingBallClick()` functions provide programmatic entry points for keyboard shortcuts or browser action buttons to control the translation state without direct user interaction with the ball.

### Configuration Persistence

User preferences such as `floatingBallPosition` (left or right) and `disableFloatingBall` are persisted via `saveConfig()` calls within the `onPositionChanged` handler. This ensures that the ball reappears in the same location across browser sessions and respects the user's enabled/disabled preference.

## Practical Implementation Examples

### Mounting the Floating Ball from a Content Script

To initialize the UI when the extension loads on a web page:

```typescript
import { mountFloatingBall } from '@/entrypoints/utils/floatingBall';

// Creates the container and mounts the Vue component
mountFloatingBall();

```

This injects the `<div id="fluent-read-floating-ball-container">` element into the DOM and activates the draggable interface.

### Triggering Translation via Keyboard Shortcut

External components can toggle the translation state without user interaction:

```typescript
// Dispatch the global event from a background script or hotkey handler
document.dispatchEvent(new CustomEvent('fluentread-toggle-translation'));

```

The listener in [`floatingBall.ts`](https://github.com/bistutu/fluentread/blob/main/floatingBall.ts) forwards this to the component, which executes `toggleTranslation()` and updates the visual state.

### Handling Translation State Changes in Parent Components

When embedding the ball in a larger Vue application:

```vue
<FloatingBall
  :position="config.floatingBallPosition"
  @onTranslationToggle="handleTranslationToggle"
/>

```

```typescript
function handleTranslationToggle(isTranslating: boolean) {
  console.log('Translation active:', isTranslating);
  // Sync with extension state or update badge icons
}

```

### Persisting Position After User Drag

The utility automatically handles persistence when the user drags the ball to a new side:

```typescript
// Inside floatingBall.ts (lines 43-49)
onPositionChanged: (newPosition: 'left' | 'right') => {
  config.floatingBallPosition = newPosition;
  saveConfig(); // Persists to extension storage
}

```

## Key Files and Their Responsibilities

| File | Role | Direct Link |
|------|------|-------------|
| [`components/FloatingBall.vue`](https://github.com/bistutu/fluentread/blob/main/components/FloatingBall.vue) | Vue 3 component handling rendering, drag physics, animations, and custom event dispatching. | [View on GitHub](https://github.com/bistutu/fluentread/blob/main/components/FloatingBall.vue) |
| [`entrypoints/utils/floatingBall.ts`](https://github.com/bistutu/fluentread/blob/main/entrypoints/utils/floatingBall.ts) | Mounting utility, lifecycle management, and bridge between UI events and translation engine. | [View on GitHub](https://github.com/bistutu/fluentread/blob/main/entrypoints/utils/floatingBall.ts) |
| [`entrypoints/utils/config.ts`](https://github.com/bistutu/fluentread/blob/main/entrypoints/utils/config.ts) | Central configuration store for `floatingBallPosition` and `disableFloatingBall` flags. | [View on GitHub](https://github.com/bistutu/fluentread/blob/main/entrypoints/utils/config.ts) |
| [`entrypoints/main/trans.ts`](https://github.com/bistutu/fluentread/blob/main/entrypoints/main/trans.ts) | Core translation logic (`autoTranslateEnglishPage`, `restoreOriginalContent`) triggered by ball interactions. | [View on GitHub](https://github.com/bistutu/fluentread/blob/main/entrypoints/main/trans.ts) |
| [`styles/theme.css`](https://github.com/bistutu/fluentread/blob/main/styles/theme.css) | CSS keyframes (`pulse-green`, `pulse-blue`) and styling for drag states and ripple effects. | [View on GitHub](https://github.com/bistutu/fluentread/blob/main/styles/theme.css) |

## Summary

- The **floating ball UI** is implemented as a self-contained Vue 3 component in [`components/FloatingBall.vue`](https://github.com/bistutu/fluentread/blob/main/components/FloatingBall.vue) that manages its own rendering, state, and animations.
- **Drag and drop functionality** tracks mouse movements with viewport clamping, distinguishes clicks from drags using time and distance thresholds, and persists the final position via [`entrypoints/utils/floatingBall.ts`](https://github.com/bistutu/fluentread/blob/main/entrypoints/utils/floatingBall.ts).
- **Translation state management** relies on custom DOM events (`fluentread-translation-started`, `fluentread-toggle-translation`) to decouple the UI from the translation engine in [`entrypoints/main/trans.ts`](https://github.com/bistutu/fluentread/blob/main/entrypoints/main/trans.ts).
- **Configuration persistence** automatically saves the ball's side position and visibility state through the `saveConfig()` utility, ensuring user preferences survive browser sessions.
- **Programmatic control** allows external extension components (such as hotkey handlers) to toggle the ball's translation state without direct user interaction.

## Frequently Asked Questions

### How does the floating ball UI distinguish between a click and a drag operation?

The component measures both time and distance during the mouse interaction. If the mouse-down duration exceeds 200 milliseconds or the pointer moves more than 5 pixels, the gesture is classified as a drag rather than a click. This prevents accidental translation toggles when the user intends to reposition the ball.

### Can I programmatically control the floating ball from a background script?

Yes. The floating ball listens for the custom event `fluentread-toggle-translation` on the document. Dispatch this event from any extension context to trigger the same translation toggle that occurs when clicking the ball manually. The [`floatingBall.ts`](https://github.com/bistutu/fluentread/blob/main/floatingBall.ts) utility forwards this event to the component's `handleExternalToggle` method.

### Where is the floating ball's position saved when I drag it to a new side?

When you release the drag, the `stopDrag` method calculates whether the ball is closer to the left or right viewport edge and emits `onPositionChanged`. The mounting utility in [`entrypoints/utils/floatingBall.ts`](https://github.com/bistutu/fluentread/blob/main/entrypoints/utils/floatingBall.ts) receives this callback, updates `config.floatingBallPosition`, and calls `saveConfig()` to persist the value to the extension's storage.

### What CSS animations power the floating ball's visual feedback?

The component uses CSS keyframes defined in the theme stylesheet (typically [`styles/theme.css`](https://github.com/bistutu/fluentread/blob/main/styles/theme.css)) to create pulse effects. When translation activates, the `triggerAnimation` method applies classes that trigger `pulse-green` or `pulse-blue` animations, while `addRippleEffect` injects a temporary `div` with the `ripple` class to create expanding circle feedback.