About this animation
An expanding comment tree with drawn connectors and rolling likes, with configurable comments, labels, fonts, and connector color.
Why this one
Replies emerge from curved branches instead of appearing all at once. A fine six-particle heart celebration and rolling counter give a small action a satisfying finish. The Lumen demo pairs warm paper, original coffee artwork, and a real glass composer with a working reply flow.
How it works
- Parent and reply layouts are measured with
onLayout, including each branch endpoint. - One interruptible progress value per thread drives its measured height, Skia path trim, chevron rotation, and reply reveal. The opening spring uses damping 56 and stiffness 300 (effective mass 4); collapse takes 200 ms.
- Reply progress windows stagger the reveal. Each reply travels 8 points from the connector and scales from 0.96 without overshoot.
- One shared value per like animates the heart, six radial strokes, and two counter layers over 300 ms. Both counter layers fade by distance and rest at integer values.
- Gesture Handler owns taps and double taps. Press feedback has an independent value, and callbacks return to JavaScript through
scheduleOnRN. - Reduced motion removes travel, rotation celebration, and particles, revealing all replies with a shared fade.
Install
npx expo install @shopify/react-native-skia expo-image react-native-gesture-handler react-native-reanimated react-native-workletsCopy this folder without demo/. Wrap your app in GestureHandlerRootView. Follow the installed Skia version's CanvasKit setup for web, and load this component after CanvasKit is ready (for example, with Skia's WithSkiaWeb boundary). The demo additionally uses expo-blur, expo-linear-gradient, and app-loaded Geist / Instrument Serif fonts. Icons: Lucide (ISC).
Usage
import CommentThreadExpand from './comment-thread-expand';
const comments = [{
id: 'a', name: 'Taylor', initials: 'TA',
text: 'A lovely way to start the day.', likes: 4,
replies: [{ id: 'b', name: 'Sam', initials: 'SA', text: 'A little room to breathe.', likes: 1 }],
}];
export function Comments() {
return <CommentThreadExpand comments={comments} />;
}<CommentThreadExpand
ref={threadRef}
comments={comments}
colorScheme="light"
accentColor="#F43F5E"
springDamping={56}
springStiffness={300}
fontFamily="YourBodyFont"
mediumFontFamily="YourMediumFont"
monoFontFamily="YourNumberFont"
onReply={comment => openComposer(comment.id)}
onLikeChange={(id, liked) => saveLike(id, liked)}
/>CommentThreadExpandRef exposes setExpanded(id, expanded), setLiked(id, liked), and reset(). Autoplay belongs to the demo, which expands at 450 ms and likes the first reply at 1050 ms, looping every 5100 ms. The list supports one level of replies; IDs must be unique. Like and expansion states are local, with callbacks for persistence. Change a row's ID to initialize it as a new comment.
Props
| Prop | Type | Default | Purpose |
|---|---|---|---|
| comments | readonly ThreadComment[] | required | Names, initials, text, timestamps, counts, tints, and replies |
| fontFamily | string | system | Body text |
| mediumFontFamily | string | system | Names and actions |
| monoFontFamily | string | system | Tabular rolling count |
| accentColor | string | #F43F5E | Heart and particles |
| lineColor | string | #D6CEC2 / #544B42 | Curved connector stroke |
| colorScheme | light / dark / auto | auto | Text and connector palette |
| springDamping | number | 56 | Opening damping, bounded to ζ ≥ 0.8 with effective mass 4 |
| springStiffness | number | 300 | Opening response |
| onExpandedChange | (id, expanded) => void | — | Expansion callback |
| onLikeChange | (id, liked) => void | — | Like callback |
| onReply | (comment) => void | — | Composer integration |
| style | StyleProp<ViewStyle> | — | Scroll container style |
| testID | string | — | Test identifier |
replyLabel | string | undefined | Label of each comment's reply action. |
comments | readonly ThreadComment[] | Required | Comments and their replies. |
fontFamily | string | undefined | Body font; defaults to the system font. |
mediumFontFamily | string | undefined | Name and action font. |
monoFontFamily | string | undefined | Rolling number font. |
accentColor | string | undefined | Heart and active action color. |
lineColor | string | undefined | Thread connector color. |
colorScheme | 'light' | 'dark' | 'auto' | undefined | Theme, following the device by default. |
springDamping | number | undefined | Opening spring damping, bounded to a damping ratio of at least 0.8. |
springStiffness | number | undefined | Opening spring stiffness. |
onExpandedChange | ( id: string, expanded: boolean, ) => void | undefined | Called when a thread changes state. |
onLikeChange | ( id: string, liked: boolean, ) => void | undefined | Called when a comment changes its like state. |
onReply | (comment: ThreadComment) => void | undefined | Called by the Reply action. |
style | StyleProp<ViewStyle> | undefined | Scroll container style. |
testID | string | undefined | Test identifier. |
hideRepliesLabel | string | Hide replies | Action copy for collapsing an expanded thread. |
formatReplyCount | (count: number) => string | View <count> replies | Formats the action copy for expanding a thread. |
Compatibility
Expo SDK 58; React Native 0.88.0; Reanimated 4.7.1; Worklets 0.13.0; Skia 2.14.0. Dependencies are generated from component imports in piece.json.
The demo uses BlurTargetView and the SDK 58 Android blur API (Expo BlurView documentation). The composer reserves 112 points of scroll padding; the thumbnail is bundled, never fetched remotely.
Accessibility
Actions have button roles, descriptive labels, 44-point minimum targets, and screen-reader activation. Thread toggles expose expanded; hearts expose selected. Collapsed replies are removed from accessibility and pointer interaction. Double tap is an optional like shortcut; the labeled heart is the equivalent single-tap action. Reduced motion uses a 180 ms simultaneous reply fade, without particles or reply transforms. Use the Reply callback to focus your own composer.
Implementation notes
Text wraps naturally without fixed-height comment rows. The 320 × 694 and 390 × 844 layouts reserve the top-right viewer close area; the article has a flexible text column and comments scroll rather than crowd the bottom composer. At 320 points, the first reply is visible before the 1050 ms autoplay like. No effect is created with an opaque block over content: the connector is a trimmed stroke, the burst is six strokes, and the composer uses actual blur.




