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 itemsiconType: Determines which iconography to rendershowShortcutTooltipandshortcutTip: 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.vuethat 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 inentrypoints/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:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →