About this animation
A long-press reaction picker with a magnifying dock and configurable messages, reaction count, background color, and fonts.
Why this one
Most reaction rows jump between fixed active states. This version derives every icon's scale, lift, label, and neighbor spacing from the finger's exact horizontal position, so the dock feels direct at any drag speed. Selection then becomes a legible event: the chosen reaction follows a curved path, settles into a persistent count chip, and releases a restrained eight-spark burst.
How it works
- A 350 ms long press activates a pan gesture over the message and opens the glass reaction capsule from the touch area.
- One continuous
fingerXshared value drives a Gaussian influence curve for all six reactions. Nearby icons enlarge and rise while adjacent icons move aside. The reaction row sits outside the clipped glass background, and magnified discs keep at least 16 pt of clearance from the container edges. - Releasing projects the current horizontal position to the nearest reaction and reports it on the JavaScript thread.
- The selected Lucide icon travels along a quadratic curve to the target message, then a spring reveals the persistent count chip.
- The component measures its own container, so the conversation, target, picker, and flight destination adapt without reading the window size.
Install
npx expo install expo-blur expo-image expo-linear-gradient @shopify/react-native-skia react-native-gesture-handler react-native-reanimated react-native-workletsUsage
import MagnifyReactionPicker from './MagnifyReactionPicker';
export function AlbumChat() {
return (
<MagnifyReactionPicker
title="Weekend album"
messages={messages}
targetMessageId="cover-photo"
onReactionSelect={({ label }) => saveReaction(label)}
/>
);
}Use the ref to drive a showcase or select a reaction programmatically:
import { useRef } from 'react';
import MagnifyReactionPicker, {
type MagnifyReactionPickerRef,
} from './MagnifyReactionPicker';
export function ReactionPreview() {
const pickerRef = useRef<MagnifyReactionPickerRef>(null);
return (
<MagnifyReactionPicker
ref={pickerRef}
brandName="Camera"
sentColorStart="#6D5BFF"
sentColorEnd="#8B5CF6"
reactionCount={3}
messages={messages}
targetMessageId="favorite"
/>
);
}The forwarded ref exposes open(), select(index), play(), and reset().
Props
| Prop | Type | Default | Description |
|---|---|---|---|
title | string | 'Shared album' | Conversation heading. |
subtitle | string | '4 members' | Supporting member count. |
brandName | string | 'Camera' | Product name shown beside the brand tile. |
memberInitials | readonly [string, string, string] | ['AM', 'JR', 'SK'] | Initials used by the overlapping member avatars. |
messages | readonly ReactionMessage[] | DEFAULT_MESSAGES | Five messages used to compose the thread. |
targetMessageId | string | 'target' | ID of the message that receives the reaction picker. |
timeLabel | string | '9:41 AM' | Time label placed between message groups. |
initialReaction | number | -1 | Index selected when the component mounts; -1 shows no chip. |
reactionCount | number | 1 | Count shown in the selected reaction chip. |
backgroundColor | string | '#0B0B0F' | Dark screen color. |
sentColorStart | string | '#6D5BFF' | First color of sent-message bubbles. |
sentColorEnd | string | '#8B5CF6' | Second color of sent-message bubbles. |
fontFamily | string | undefined | Typeface for body copy. |
mediumFontFamily | string | undefined | Typeface for headings and labels. |
monoFontFamily | string | undefined | Typeface for time and reaction counts. |
onReactionSelect | (reaction: { index: number; label: string }) => void | undefined | Called when a reaction settles on the message. |
accessibilityLabel | string | 'Message reaction picker' | Screen-reader label for the target message. |
style | StyleProp<ViewStyle> | undefined | Optional style for the outer container. |
testID | string | undefined | Test identifier applied to the outer container. |
Compatibility
- Expo SDK 58
- React Native 0.88
- Reanimated 4.7 and Worklets 0.13
- Skia 2.14
- iOS: supported
- Android: supported
- Web: supported
- Works in Expo Go because all native dependencies are included in Expo SDK 58
Accessibility
Every reaction is a labeled button with a 44 pt hit area, and the target exposes an accessibility action that opens the picker. Tapping any reaction is an alternative to dragging. With reduced motion enabled, magnification, message scaling, arc flight, spring bounce, and particles are removed; opening and selection use short fades instead.
Customization notes
The picker follows the gesture and springs; the optional sweep keeps its scheduled selection synchronized.




