Skip to content
Native Animation
Inline source reveal animation preview

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

npx nativeanimation add inline-source-reveal

Inline source reveal

Inline citations that open an evidence panel and return to their marker, with configurable text segments, sources, 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

Inline citations that open an evidence panel and return to their marker, with configurable text segments, sources, palette, and fonts.

Why this one

Reading position is part of the citation contract. Evidence opens inside the component without moving the paragraph; sources belong to a claim, and Close returns focus to its exact marker. There is no hover dependency, remote favicon, global portal, automatic navigation, or factual verification.

How it works

  1. Pass stable text/claim segments and host-owned source objects. Claims resolve their source IDs locally, preserving their order and removing missing IDs and duplicates.
  2. Words wrap in normal React Native layout. Each citation owns a real 48 × 48 control in that same flow, with a 24 × 24 visual marker. No hit slop overlaps another line.
  3. A reserved evidence area stays in layout even when closed. A spring drives its opacity, vertical offset, and corner radius. A separate 320 ms underline travels backward from the citation through only the selected claim; the title rule completes the connection.
  4. Source changes fade out for 80 ms, pause for 20 ms, then fade in for 120 ms. Only one evidence snapshot is mounted. Removed evidence is hidden immediately; available sources are resolved again from the current props.
  5. Selection and callbacks stay on JS. Timers, keyboard/back subscriptions, and UI animations are cleaned up on unmount.

Install

Copy this folder without demo/ into your app. React and React Native are peer requirements.

npx expo install @shopify/react-native-skia react-native-reanimated react-native-worklets

Verified dependency versions: Skia 2.14.0 and Reanimated 4.7.1, with Expo SDK 58 / React Native 0.88.0 and the project's Reanimated/worklets setup. Web requires Skia's CanvasKit initialization before importing this component. Icons: Lucide (ISC); exact paths copied from the internal kit.

Usage

import { InlineSourceReveal, type Source } from './inline-source-reveal';

const sources: Source[] = [{
  id: 'reading', title: 'Reading habits, revisited',
  domainLabel: 'research.example', url: 'https://research.example/reading',
  excerpt: 'Short sessions were easier to repeat over time.',
}];

<InlineSourceReveal
  segments={[{
    id: 'routine', type: 'claim',
    text: 'Small daily reading sessions help a routine stick.',
    sourceIds: ['reading'], citationLabel: '1',
  }]}
  sources={sources}
  labels={{
    sourceHeading: 'Evidence', openSource: 'Open source', close: 'Close source',
    previous: 'Previous source', next: 'Next source', unavailable: 'Source unavailable',
  }}
  accessibilityLabel="Reading response with citations"
  onOpenSource={source => showSourceDetail(source)}
/>

The host implements showSourceDetail. It may show a full accessible detail screen or open a validated URL. The component never invokes a browser or fetches a source.

Controlled selection and appearance knobs:

const [claim, setClaim] = useState<string | null>(null);
const evidenceRef = useRef<InlineSourceRevealRef>(null);

<InlineSourceReveal
  {...contentProps}
  ref={evidenceRef}
  activeClaimId={claim}
  onActiveClaimChange={setClaim}
  onSourceChange={sourceId => recordSourceSelection(sourceId)}
  colorScheme="dark"
  motionScale={0.8}
  fontFamily={bodyFont}
  titleFontFamily={mediumFont}
  monoFontFamily={numberFont}
  formatSourcePosition={(index, total) => `${index + 1}/${total}`}
/>

Props

PropDefaultContract
segments: Segment[]RequiredOrdered plain text or claims with unique stable IDs. Claim: id, text, sourceIds, citationLabel. Plain text: id, type: 'text', text. Either supports paragraphBreakBefore to start a new paragraph.
readingFontSize?: number16 compact / 18 regularReading font size in points, with 1.45× line height. Controls keep their interface typography.
sources: Source[]RequiredUnique stable id, title, domainLabel, url, excerpt; optional dateLabel disappears when empty.
activeClaimId?: string | nullUncontrolled, closedControlled props prevail over imperative requests. Invalid selection closes the presentation. Do not switch modes during an instance's lifetime.
onActiveClaimChange?(id)—Requests opening or closing once per action. Reopening the current marker closes it.
onOpenSource(source)RequiredExplicit business action only; disabled for missing/empty URLs. Receives the current full source.
onSourceChange?(sourceId)—Explicit Previous/Next selection only; filtered claim-relative order, bounded ends.
labelsRequiredSee the complete label table below.
formatSourcePosition?(index,total)1/2Zero-based index; default position has a reserved 56 pt tabular column. Supply a concise localized value.
colorScheme?: 'light' | 'dark' | 'auto'autoComplete Paper/Night appearance.
palette?: PaletteTheme paletteFull background, surface, text, muted, border, accent, onAccent, success, error override. Host owns the surrounding background.
fontFamily?, readingFontFamily?, titleFontFamily?, monoFontFamily?SystemHost-loaded fonts; no global theme imports.
motionScale?: number1Clamped 0–1.5; nonfinite values use 1. Zero removes travel and sweep.
style?: StyleProp<ViewStyle>—Root uses flex:1, measured with onLayout. No screen dimensions or safe-area padding.
testID?: stringinline-source-revealSuffixes: -citation-ID, -close, -open-source, -previous, -next.
accessibilityLabel: stringRequiredDescribes the reading region.
Label keyDemo valueUsed for
openSourceOpenVisible CTA and accessible name.
closeClose sourceClose icon accessible name.
previousPrevious sourcePrevious control accessible name.
nextNext sourceNext control accessible name.
unavailableSource unavailableEmpty or missing evidence fallback and announcement.
sourceHeadingEvidencePanel heading and marker accessible name prefix.

Ref and controlled behavior

open(claimId), close(), nextSource(), previousSource(), and reset() are exposed by InlineSourceRevealRef. Open/close request onActiveClaimChange; they never mutate a controlled claim. Next/Previous operate on local source selection and notify onSourceChange. reset() clears local source selection and uncontrolled selection without any business callback; it leaves a controlled claim intact. The host should discard stale asynchronous responses by ID before replacing props.

States and transitions

State / triggerResult
Closed → citation / ref openClaim stays stationary; panel spring and underline reveal.
Open → another citationRetarget from current reveal progress; evidence changes serially; new marker becomes return target.
Open → same citation / Close / Escape / Android BackHide evidence; restore marker focus. Back is consumed while open.
Previous / NextBounded claim-relative source change; callback at selection time, independent of animation completion.
Removed sourceRe-resolve available IDs, fall back to first remaining source; none means unavailable with disabled link and Close.
Missing claim / empty segmentsClosed presentation; no exception.
Reduced motion / motionScale 0No travel or sweep; evidence uses 90 ms exit, 20 ms pause, 90 ms entry.

Demo and layout

Lumen, the article “The quiet art of reading,” its reading tips, three citations, evidence, domains, and URLs are fictional. The article uses Instrument Serif at 1.18× its nominal size, independently of the Geist interface. “Open” stays on one line and demonstrates the host callback with a local detail card; it does not navigate. Replay and Night/Paper stay below the content.

Autoplay opens claim 1 at 600 ms, changes its source at 1900 ms, closes at 2700 ms, opens claim 2 at 3400 ms, switches to claim 3 at 4200 ms, closes at 4900 ms, and repeats at 5500 ms. Pointer/touch/keyboard interaction cancels queued playback.

The demo uses no scroll at 320 × 568, 320 × 694, or 390 × 844. Short reading tips and a compact evidence panel preserve room for the 48 pt controls. At large system text, one outer scroll container keeps the content reachable with its indicator hidden. At 200% text it retains the first claim and removes the brand/headline. Landscape removes the brand, reduces headline spacing, and places reading and evidence side by side. The source CTA gets its own row at large text. Long source previews are bounded; full values pass unchanged to the host callback.

Compatibility

Targets Expo SDK 58, React Native 0.88.0, Reanimated 4.7.1, and Skia 2.14.0 on iOS, Android, and web. These native modules are available in the matching Expo Go client. Initialize CanvasKit before mounting on web.

Accessibility

React Native Text retains system scaling. Controls are 48 pt minimum, with press-in feedback. Web Pressable provides Enter/Space and Tab; Escape closes. Closing restores web focus to the originating citation with preventScroll. The evidence label is plain, nonfocusable text on web; opening never transfers browser focus to it. Native accessibility focus uses a platform-guarded node handle and announces only the current source. Closed/transitioning content is excluded from accessibility; disabled actions expose their state. The host owns full source detail and should provide a vertical detail layout for large text, including the untruncated title/domain/excerpt.

Implementation notes

  • All interface and evidence text comes from props; empty date labels leave no spacer. All demo controls stay away from the upper-right viewer dismissal area.
  • The paragraphs keep identical geometry before/after opening. Markers own actual flow space, not overlapping hit slop. No labels crossfade or stack; missing evidence cannot remain actionable.
  • The first autoplay reveal starts at 600 ms and its fine underline completes before 1 second. It never covers letters. No rolling-number effect is needed for the static source fraction.
  • Layout budgets: normal reading minimum 160 pt, compact 136 pt, evidence slot minimum 204 pt compact / 220 pt regular; controls remain 48 pt. Compact reading omits paragraph spacers. Landscape uses columns. No safe area is applied twice.
  • There are no BlurViews or permanent ambient animation, so no square blur artifacts or idle rendering clock. All springs specify mass 1.
  • Business navigation is tied to the explicit CTA, never to animation completion. Source IDs are filtered against current host data.