Skip to content
Native Animation
Read Along Highlight animation preview

This preview uses fictional demo content. See the props below for the customization options available in this component.

npx nativeanimation add read-along-highlight

Read Along Highlight

A read-along text marker with line-aware highlight transfers, with configurable timed tokens, labels, palette, and fonts.

$9 for this animation

Add to your project

Copy the component and its assets into your app with the Native Animation CLI, or paste the agent prompt into Claude Code, Codex or Cursor once the MCP server is connected.

About this animation

A read-along text marker with line-aware highlight transfers, with configurable timed tokens, labels, palette, and fonts. Your app supplies playback time and seeking; no audio or speech synthesis is included.

How it works

  1. Validate unique IDs and ordered, finite, nonoverlapping timestamps. Binary search selects exactly one word in [start, end); gaps select nothing.
  2. Render words and actual space characters in a wrapping flow. Each word has an onLayout measurement; a second measurement locates its ink inside the wrapping slot. The paragraph never moves to accommodate the marker.
  3. Copy currentTime to the master readProgress shared value. Derive the active token, within-word light, and overall progress on the UI runtime. There is no internal React playback clock.
  4. Move the marker in 100 ms on a row. Across rows, collapse at the outgoing edge, switch rows while invisible, and expand at the incoming edge (60 + 60 ms by default, capped at 180 ms with duration overrides). Interrupted transfers start from the current pose. Gaps immediately hide the marker.
  5. Follow only outside the central 20–80% reading band. Programmatic scrolling uses a 180 ms UI animation. Manual scrolling suspends follow; the Follow reading button restores it. Seeking requests host time once and never starts playback.

Local spacing: 8/10/16/20 pt; 24 pt paper radius, 28 pt player radius. A single paragraph tap surface maps taps to the nearest word on that line, including adjacent whitespace, so short words keep natural spacing. Previous/Next controls reserve at least 48 × 48. Reading ink defaults to 33.04/42 pt. Interface text is 14/20; elapsed time is 18/26. Font scaling remains enabled.

Install

npx expo install react-native-gesture-handler react-native-reanimated react-native-worklets

Copy this folder without demo/ into your project. SDK 58 configures the Worklets Babel plugin through babel-preset-expo. React 19, the New Architecture, and a bounded parent are required. Wrap the app in GestureHandlerRootView. The component does not need Skia, assets, navigation, or a safe-area provider. The showcase alone uses Skia, the host's loaded fonts, and safe-area context.

Usage

import { useState } from 'react';
import ReadAlongHighlight from './read-along-highlight';

const tokens = [
  { id: 'notice', text: 'Notice', start: 0, end: 0.6 },
  { id: 'more', text: 'more.', start: 0.8, end: 1.4 },
];

export function Reading() {
  const [time, setTime] = useState(0);
  const [playing, setPlaying] = useState(false);
  // Your audio player supplies time and consumes playing.
  return (
    <ReadAlongHighlight
      tokens={tokens}
      currentTime={time}
      playing={playing}
      onSeek={setTime}
      onPlayPause={() => setPlaying(value => !value)}
      style={{ minHeight: 350 }}
    />
  );
}

Personalize the material and motion without changing timing semantics:

<ReadAlongHighlight
  tokens={tokens}
  currentTime={playerTime}
  playing={playerIsPlaying}
  onSeek={requestPlayerSeek}
  onPlayPause={togglePlayer}
  colorScheme="dark"
  palette={{ highlight: '#584725', accent: '#E4BC65' }}
  titleFontFamily="YourReadingFont"
  readingFontSize={30}
  readingLineHeight={42}
  motion={{ durationScale: 0.8, glowOpacity: 0.2 }}
  labels={{ previousWord: 'Previous word', nextWord: 'Next word' }}
  formatTime={seconds => `${seconds.toFixed(1)}s`}
/>

Props

PropDefaultPurpose
tokens: readonly ReadAlongToken[]RequiredStable ID, text, inclusive start, exclusive end, seconds
currentTime: numberRequiredHost-owned time; negative values clamp to zero
playing: booleanRequiredHost-owned playback; ended/invalid is presented as paused
onSeek(seconds)RequiredOne callback per seek action, including reset
onPlayPause()RequiredPlayback toggle request
labelsNeutral Englishplay, pause, follow, elapsed(seconds), empty, invalid, previousWord, nextWord
formatTime(seconds)m:ssVisible elapsed and total time
autoFollowtrueInitial follow policy; changing the prop restores that policy
onFollowChange(following)—Discrete changes only
highlightPadding3Clamped 0–12 pt
colorSchemeautolight, dark, auto
palettePaper / NightPartial override of complete semantic palette
fontFamily, titleFontFamily, monoFontFamilySystemInterface, reading, and time fonts
readingFontSize, readingLineHeight33.04, 42Positive sizes; line height at least 1.15× font size
motion{ durationScale: 1, glowOpacity: 0.25 }Duration 0.5–2, glow 0–0.3; nonfinite overrides use defaults
disabledfalseBlocks touch and handle actions except reset
style, testID, accessibilityLabel—Root style last; stable control IDs; paragraph name
ref—Typed React 19 action handle

The palette exposes background, surface, elevated, text, muted, border, accent, onAccent, accentSoft, glow, success, error, scrim, track, focus, plus highlight, edge, shadowNear, and shadowFar.

Handle: seekToToken(id) requests the token's start once; follow() resumes following; reset() cancels pending movement, restores the initial follow policy, scrolls to the start, and requests time zero. No method changes the controlled time or playback state. Unknown IDs do nothing. In gaps, Previous/Next seek to the nearest earlier/later word start. At the first/last word, unavailable actions are disabled.

Invalid timestamps, duplicate/empty IDs, empty word text, overlaps, or nonfinite time disable seeking/playback and expose labels.invalid; existing text remains readable. An empty list exposes labels.empty. Unmeasured words remain visible without a marker. Replace data immutably when changing it. A single token can wrap if it is wider than the host; ordinary word tokens produce one marker rectangle each.

The component owns its reading scroll and uses a fallback outer scroll for large controls. It measures its own viewport; the host owns safe areas. Use a bounded height, for example style={{ width: 300, height: 400, flex: 0 }}. Font/width changes invalidate word geometry; no raster text is cached.

Compatibility

Target stack: Expo SDK 58, React Native 0.88.0, React 19.2.3, Reanimated 4.7.1, Worklets 0.13.0. iOS, Android, web, and Expo Go are intended targets.

Accessibility

One continuous accessible paragraph represents the reading, without per-word announcements. The visual paragraph tap surface is hidden from accessibility and keyboard focus; labeled Previous word / Next word controls provide equivalent seek actions. Controls support keyboard Tab/Enter and expose disabled/selected state and a focus border. No automatic live-region announcements follow the clock. RTL uses logical wrapping and reverses transfer edges.

useReducedMotion() supplies the initial preference; an AccessibilityInfo subscription handles live changes. Reduced motion places the marker immediately on the active word and follows without scroll animation. Text, timestamps, controls, and seek requests remain available. Paper and Night preserve full ink color beneath the marker. All control targets reserve at least 48 pt; at large text sizes controls wrap in flow and the outer fallback scroll keeps them reachable.

Why this one

Unlike a fixed quote marker, the reader accepts an external media timeline, handles silent gaps, crosses wrapped lines without a diagonal sweep, and supports seeking without taking ownership of playback.

The Lumen showcase uses original copy and synthetic timestamps. The supplied sentence contains eleven words while the brief lists twelve timing slots: the eleventh slot is left silent and the final word uses slot twelve, ending at 4.10 s. The five-second autoplay pauses at 2.8 s, seeks back at 3.3 s, and resets at 4.2 s. Real touch or backgrounding cancels it. Raster assets: N/A. No audio is shipped. Icons: Lucide (ISC), exact local kit lamp path; only used in the demo. No reference assets are distributed.