How the Read-Frog Text Selection Toolbar Enables Instant Translation, AI Explanation, and Speech Playback
The Read-Frog text selection toolbar detects user text highlights through native browser selection APIs, positions a floating React interface based on drag direction, and streams LLM-powered translations or AI explanations while offering instant text-to-speech synthesis.
The read-frog repository implements a sophisticated text selection toolbar as a content script that injects directly into web pages, providing immediate access to language tools without disrupting the browsing experience. This component tracks selection geometry, manages complex viewport positioning, and interfaces with configurable AI providers to deliver real-time assistance.
Detecting Text Selections and Calculating Direction
The toolbar lifecycle begins with mouse event handlers in src/entrypoints/selection.content/selection-toolbar/index.tsx. When a user initiates a selection, handleMouseDown records the starting cursor position in selectionStartRef and hides any existing toolbar instance.
Upon mouse release, handleMouseUp executes within a requestAnimationFrame callback to ensure the native selectionchange event fires first. The handler performs several validation steps:
- Verifies the click target is not an unrelated button using a
containsNodeguard - Retrieves the current selection via
window.getSelection() - If the selection contains text, it persists the content to
selectionContentAtomand theRangeobject toselectionRangeAtom
Crucially, the component calculates selection direction (top-left, top-right, bottom-left, or bottom-right) by comparing the initial mouse coordinates from selectionStartRef with the current mouse position. The getSelectionDirection function (lines 21-36) determines which corner of the selection the toolbar should anchor to, ensuring the UI appears on the same side as the user's drag motion.
The raw client coordinates (docX, docY) are stored in selectionPositionRef, and the toolbar visibility atom (isSelectionToolbarVisibleAtom) is set to true.
Positioning the Floating Toolbar
Positioning logic resides in the updatePosition function, invoked from a useLayoutEffect hook and during scroll events. Rather than triggering React re-renders, the component writes positioning values directly to the DOM for performance:
const { x: offsetX, y: offsetY } = applyDirectionOffset(
selectionDirectionRef.current,
selectionPositionRef.current.x,
selectionPositionRef.current.y,
tooltipWidth,
tooltipHeight,
);
The applyDirectionOffset helper (lines 45-55) applies directional offsets—for example, subtracting tooltip dimensions for top-left selections or maintaining original coordinates for bottom-right. The function then clamps the final coordinates to viewport margins (defined by the MARGIN constant in src/utils/constants/selection.ts) and document boundaries (lines 93-101), preventing the toolbar from rendering off-screen.
Direct DOM manipulation occurs via tooltipRef.current.style.top and tooltipRef.current.style.left. To maintain positioning during page scrolling, the handleScroll method (lines 103-110) schedules updatePosition on the next animation frame.
Core Action Buttons
The toolbar exposes three primary actions through dedicated React components, each consuming shared Jotai state from src/entrypoints/selection.content/selection-toolbar/atom.ts.
Translation with Streaming Response
The TranslateButton component (lines 31-40 of translate-button.tsx) captures the click location in mouseClickPositionAtom and toggles the TranslatePopover visibility.
Inside TranslatePopover, the implementation:
- Sanitizes selected text by removing zero-width spaces:
replace(/\u200B/g, "").trim() - Determines if the configured provider is an LLM via
isLLMProviderConfig - Constructs prompts using
getTranslatePrompt - Streams the response through
streamBackgroundText, updating state incrementally - Manages request cancellation through an
AbortController
The popover UI includes a loading indicator, the translated result, a clipboard copy button, and a "Speak Original" button that reuses the text-to-speech hook.
Text-to-Speech Playback
The SpeakButton (lines 9-27 of speak-button.tsx) integrates with the useTextToSpeech hook. It reads selectionContentAtom and TTS configuration from configFieldsAtomMap.tts. On activation, it either stops ongoing playback via stop() or initiates synthesis through play(selectionContent, ttsConfig). The button displays dynamic icons representing loading, active playback, or idle states.
AI Explanation
The AiButton and AiPopover components (implemented in ai-button.tsx) follow the same architectural pattern as the translation flow, specifically invoking the selectionToolbar.ai provider configuration. This feature streams generated explanations using identical streaming infrastructure, presenting AI-generated context for the selected text without requiring page navigation.
State Management with Jotai
All transient UI state lives in atoms defined in selection-toolbar/atom.ts (lines 3-13). The architecture uses useAtom and useAtomValue hooks to maintain pure, testable logic separate from UI components. The test suite in src/__tests__/selection.content/selection-toolbar/selection-toolbar.test.tsx validates visibility toggles, direction handling, boundary clamping, and scroll update behaviors.
Configuration and Site-Specific Disabling
Users can disable the toolbar on specific domains through selectionToolbar.disabledSelectionToolbarPatterns. Before rendering, the component checks these patterns using matchDomainPattern (lines 38-40 of index.tsx) and suppresses the toolbar when the current site matches a disabled pattern.
Extending with Custom Features
The toolbar supports Custom Features via custom-feature-button.tsx and custom-feature-prompt.ts. Developers can inject new LLM-driven actions without modifying core toolbar code by following the established atom-driven pattern:
// src/entrypoints/selection.content/selection-toolbar/custom-feature-button.tsx
export function MyFeatureButton() {
const setIsVisible = useSetAtom(isCustomFeaturePopoverVisibleAtom);
return (
<button onClick={() => setIsVisible(true)} className="size-6 …">
<MyIcon />
</button>
);
}
Include the component in the main SelectionToolbar render tree and define a corresponding popover following the TranslatePopover implementation pattern.
Programmatic Control Examples
Rendering the Toolbar
The content script entry point injects the toolbar into the page DOM:
import { SelectionToolbar } from '@/entrypoints/selection.content/selection-toolbar';
function injectToolbar() {
const container = document.createElement('div');
document.body.appendChild(container);
createRoot(container).render(<SelectionToolbar />);
}
injectToolbar();
Triggering Translation Programmatically
External components can control toolbar state through Jotai atoms:
import { setAtom } from 'jotai';
import { mouseClickPositionAtom, isTranslatePopoverVisibleAtom } from '@/entrypoints/selection.content/selection-toolbar/atom';
// Position the popover at (100, 200) and show it
setAtom(mouseClickPositionAtom, { x: 100, y: 200 });
setAtom(isTranslatePopoverVisibleAtom, true);
Summary
- Detection: Mouse handlers in
index.tsxtrack selections viawindow.getSelection()and calculate directional offsets usinggetSelectionDirection(lines 21-36). - Positioning: The
updatePositionfunction applies directional offsets throughapplyDirectionOffset(lines 45-55), clamps to viewport margins, and writes directly to DOM refs to avoid re-render overhead. - Translation:
TranslatePopoversanitizes input, determines LLM vs. built-in providers, and streams responses viastreamBackgroundTextwithAbortControllercancellation support. - Speech:
SpeakButtoninterfaces with theuseTextToSpeechhook to synthesizeselectionContentAtomwith configurable TTS settings. - State: Jotai atoms in
atom.tsmanage all UI state, enabling testable, decoupled architecture validated bysrc/__tests__/selection.content/selection-toolbar/selection-toolbar.test.tsx. - Extensibility: The custom feature system allows plugging new AI actions without core modifications.
Frequently Asked Questions
How does the toolbar determine where to appear on screen?
The toolbar calculates selection direction (top-left, top-right, bottom-left, bottom-right) by comparing the initial mouse down coordinates with the mouse up position using getSelectionDirection in index.tsx (lines 21-36). The applyDirectionOffset function (lines 45-55) then positions the tooltip on the same side as the user's drag motion, ensuring it never obscures the selected text.
Can the toolbar be disabled on specific websites?
Yes. The toolbar checks selectionToolbar.disabledSelectionToolbarPatterns from the user configuration using matchDomainPattern (lines 38-40 of index.tsx). If the current domain matches a configured pattern, the component suppresses visibility automatically.
How is the translation text streamed to the UI?
The TranslatePopover component determines if the provider is an LLM via isLLMProviderConfig, builds a system prompt through getTranslatePrompt, and calls streamBackgroundText to receive chunks incrementally. Each chunk updates the translatedText state, creating a real-time typing effect. An AbortController manages request cancellation if the user closes the popover early.
What happens to the toolbar position when the user scrolls?
The handleScroll handler (lines 103-110) schedules updatePosition on the next animation frame. This recalculates the toolbar coordinates relative to the new scroll position while maintaining the original directional offset and viewport boundary clamping, ensuring the toolbar remains attached to the selection viewport.
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 →