About this animation
A link preview that unfolds from a note into a shared-image reader, with configurable image, preview copy, fonts, and accent color.
Why this one
The loading state has a visible rhythm: an underline draws, a spring opens the skeleton, a genuinely blurred photo resolves, the headline types, and metadata fades in. The same photo and card surface expand into a reader. Swipe dismissal preserves the URL as ordinary text; a labeled button offers the same action.
How it works
- Paste an HTTP(S) URL into the multiline note, or call
ref.paste(url). - Local URL validation selects the ready or error result. This component presents supplied preview data; it does not fetch or scrape URLs.
- A UI-thread loading progress drives the photo, headline characters, metadata, and skeleton. Separate shared values hold interruptible reveal, reader, drag, and press positions.
- A pan projects release velocity to either return the card or dismiss it. Gesture Handler composes tap and pan; there is no nested Pressable competing with the preview gesture.
- Reader expansion changes the geometry of the existing image and card, rather than mounting a replacement image.
- Semantic transitions notify
onStateChangeand assistive technology. The demo owns its cancellable autoplay timers.
Install
npx expo install expo-image expo-linear-gradient react-native-gesture-handler react-native-reanimated react-native-workletsSDK-compatible versions used here: expo-image ~58.0.17, expo-linear-gradient ~58.0.3, Gesture Handler ~3.3.0, Reanimated 4.7.1, worklets 0.13.0. Wrap your app in GestureHandlerRootView. Copy the component, icons.ts, and index.ts into your app. No Skia initialization is needed. Icons: Lucide (ISC); paths are copied from the project kit and rendered as SVG image sources.
Usage
import { useRef } from 'react';
import { Button, View } from 'react-native';
import { LinkUnfurlCard, type LinkUnfurlCardRef } from './link-unfurl-card';
export function Note() {
const preview = useRef<LinkUnfurlCardRef>(null);
return (
<View style={{ flex: 1, padding: 24 }}>
<LinkUnfurlCard
ref={preview}
image={require('./assets/forest.jpg')}
title="A quieter afternoon"
summary="Take the longer path. Notice the light between the trees."
/>
<Button title="Insert link" onPress={() => preview.current?.paste('https://journal.example/quiet')} />
</View>
);
}Personality knobs:
<LinkUnfurlCard
image={require('./assets/forest.jpg')}
accentColor="#0EA5E9"
loadDuration={1000}
springDamping={30}
colorScheme="light"
fontFamily="YourInterfaceFont"
titleFontFamily="YourEditorialFont"
onStateChange={(state) => savePreviewState(state)}
/>Props
| Prop | Type | Default | Purpose |
|---|---|---|---|
| image | ImageSourcePropType | Required | Supplied preview photo |
| title | string | A moment worth saving | Preview and reader headline |
| summary | string | Generic short summary | Scrollable reader body |
| readingTime | string | 5 min read | Metadata label |
| noteTitle | string | New note | Note heading |
| initialNote | string | Empty | Initial editable note |
| fontFamily | string | System | UI typeface |
| titleFontFamily | string | System | Editorial typeface |
| accentColor | string | #0EA5E9 | Link and action accent |
| loadDuration | number | 1200 | Loading timeline in ms, minimum 400 |
| springDamping | number | 28 | Reveal and reader damping |
| colorScheme | light / dark / auto | auto | Surface theme |
| onStateChange | (state) => void | — | JS-thread semantic transition callback |
| onInteraction | () => void | — | Touch/edit signal for stopping autoplay |
| accessibilityLabel | string | Open link preview in reader | Preview action label |
| style | StyleProp<ViewStyle> | — | Root container style |
| testID | string | — | Root identifier |
The ref exposes paste(url), expand(), close(), dismiss(), and reset(). States: idle, loading, ready, reader, dismissed, error. Dismiss keeps the URL; reset clears it. initialNote initializes the editor and supplies the reset value.
Compatibility
Expo SDK 58, React Native 0.88.0, Reanimated 4.7.1, worklets 0.13.0. Uses modules already present in this project's Expo Go environment. iOS, Android, and web are targeted. No new dependencies or native configuration changes.
Recommended allocated component height: at least 470 pt, with a width of at least 272 pt. The demo provides this space at 320×694 and 390×844. Reader content scrolls instead of shrinking type.
Accessibility
Interactive controls are labeled and at least 44 pt. The preview supports activate/dismiss accessibility actions, with explicit Read story, Dismiss, and Back to note controls outside the gesture surface. State changes are announced and the footer is a polite live region. Headline characters expose one complete accessible title. Reduced motion uses 200 ms fades, removes blur, shimmer, shake, drag flight, press scaling, and spatial interpolation; the explicit dismissal button remains available.
Demo timeline
At 350 ms the URL is inserted; the preview resolves by 1550 ms. At 2700 ms the same card expands. At 5250 ms it returns; at 6500 ms it is dismissed. The sequence restarts at 8050 ms. A touch or edit cancels autoplay immediately. Paste link, Invalid link, and Replay are bottom controls; the upper-right viewer-close area stays empty.




