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

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, while the integration and lifecycle management reside in 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 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 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 captures. The utility then invokes autoTranslateEnglishPage() from 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:

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:

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

The listener in 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:

<FloatingBall
  :position="config.floatingBallPosition"
  @onTranslationToggle="handleTranslationToggle"
/>
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:

// 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 Vue 3 component handling rendering, drag physics, animations, and custom event dispatching. View on GitHub
entrypoints/utils/floatingBall.ts Mounting utility, lifecycle management, and bridge between UI events and translation engine. View on GitHub
entrypoints/utils/config.ts Central configuration store for floatingBallPosition and disableFloatingBall flags. View on GitHub
entrypoints/main/trans.ts Core translation logic (autoTranslateEnglishPage, restoreOriginalContent) triggered by ball interactions. View on GitHub
styles/theme.css CSS keyframes (pulse-green, pulse-blue) and styling for drag states and ripple effects. View on GitHub

Summary

  • The floating ball UI is implemented as a self-contained Vue 3 component in 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.
  • 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.
  • 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 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 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) 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.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →