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 containsNode guard
  • Retrieves the current selection via window.getSelection()
  • If the selection contains text, it persists the content to selectionContentAtom and the Range object to selectionRangeAtom

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:

  1. Sanitizes selected text by removing zero-width spaces: replace(/\u200B/g, "").trim()
  2. Determines if the configured provider is an LLM via isLLMProviderConfig
  3. Constructs prompts using getTranslatePrompt
  4. Streams the response through streamBackgroundText, updating state incrementally
  5. 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.tsx track selections via window.getSelection() and calculate directional offsets using getSelectionDirection (lines 21-36).
  • Positioning: The updatePosition function applies directional offsets through applyDirectionOffset (lines 45-55), clamps to viewport margins, and writes directly to DOM refs to avoid re-render overhead.
  • Translation: TranslatePopover sanitizes input, determines LLM vs. built-in providers, and streams responses via streamBackgroundText with AbortController cancellation support.
  • Speech: SpeakButton interfaces with the useTextToSpeech hook to synthesize selectionContentAtom with configurable TTS settings.
  • State: Jotai atoms in atom.ts manage all UI state, enabling testable, decoupled architecture validated by src/__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:

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 →