# How the Read-Frog Text Selection Toolbar Enables Instant Translation, AI Explanation, and Speech Playback

> Discover how the Read Frog text selection toolbar uses browser APIs to offer instant translation, AI explanations, and speech playback. Experience seamless content understanding with this powerful tool.

- Repository: [MengXi/read-frog](https://github.com/mengxi-ream/read-frog)
- Tags: how-to-guide
- Published: 2026-03-07

---

**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`](https://github.com/mengxi-ream/read-frog/blob/main/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:

```typescript
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`](https://github.com/mengxi-ream/read-frog/blob/main/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`](https://github.com/mengxi-ream/read-frog/blob/main/src/entrypoints/selection.content/selection-toolbar/atom.ts).

### Translation with Streaming Response

The `TranslateButton` component (lines 31-40 of [`translate-button.tsx`](https://github.com/mengxi-ream/read-frog/blob/main/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`](https://github.com/mengxi-ream/read-frog/blob/main/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`](https://github.com/mengxi-ream/read-frog/blob/main/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`](https://github.com/mengxi-ream/read-frog/blob/main/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`](https://github.com/mengxi-ream/read-frog/blob/main/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`](https://github.com/mengxi-ream/read-frog/blob/main/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`](https://github.com/mengxi-ream/read-frog/blob/main/custom-feature-button.tsx) and [`custom-feature-prompt.ts`](https://github.com/mengxi-ream/read-frog/blob/main/custom-feature-prompt.ts). Developers can inject new LLM-driven actions without modifying core toolbar code by following the established atom-driven pattern:

```tsx
// 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:

```tsx
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:

```ts
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`](https://github.com/mengxi-ream/read-frog/blob/main/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`](https://github.com/mengxi-ream/read-frog/blob/main/atom.ts) manage all UI state, enabling testable, decoupled architecture validated by [`src/__tests__/selection.content/selection-toolbar/selection-toolbar.test.tsx`](https://github.com/mengxi-ream/read-frog/blob/main/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`](https://github.com/mengxi-ream/read-frog/blob/main/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`](https://github.com/mengxi-ream/read-frog/blob/main/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.