Reaction picker
Free · MITReact Native · ExpoLong-press a message and a pill of emoji springs out of it one by one; slide the finger across and the emoji under it swells dock-style while its neighbours lean in; let go to pick and the emoji flies in an arc onto the message, landing in a badge with a pop.
Installation
$ npx shadcn@latest add https://goodui.dev/native/r/reaction-picker.jsonThen run npx expo install --fix so react-native-gesture-handler, react-native-reanimated match your Expo SDK. Files land in components/goodui-native/reaction-picker.
Usage
import { useState } from "react";
import { Text, View } from "react-native";
import { ReactionPicker } from "@/components/goodui-native/reaction-picker/reaction-picker";
export function Update() {
const [reaction, setReaction] = useState<string | null>(null);
return (
<ReactionPicker
value={reaction}
onValueChange={setReaction}
count={12 + (reaction ? 1 : 0)}
>
<View style={{ padding: 12, borderRadius: 14 }}>
<Text>Shipped the new onboarding</Text>
</View>
</ReactionPicker>
);
}When to use
- Reacting to messages in a chat (ChatBubble has it built in through reaction and onReactionChange).
- Quick feedback on posts, comments, photos or team updates where one emoji is the whole reply.
- Actions such as reply, copy or delete: use context-menu (it can sit around the same element for a different gesture, or offer reactions as items).
- A single like: use like-button.
- Choosing from many emoji or searching: open a bottom-sheet with a grid.
Examples
Your own reaction set
<ReactionPicker
reactions={[
{ key: "clap", emoji: "👏", label: "Applause" },
{ key: "fire", emoji: "🔥", label: "Fire" },
{ key: "party", emoji: "🎉", label: "Celebrate" },
]}
value={reaction}
onValueChange={setReaction}
>
{card}
</ReactionPicker>On a chat bubble
<ChatBubble side="sent" reaction={m.reaction} onReactionChange={(k) => react(m.id, k)}>{m.text}</ChatBubble>API reference
| Prop | Type | Default |
|---|---|---|
| children* The message or post that gets the reaction. Keep it non-interactive and put its tap in `onPress`. | ReactNode | — |
| reactions Reactions in the pill, five to seven work best. Defaults to love, like, haha, wow, sad, thanks. | Reaction[] | — |
| value Key of the chosen reaction, `null` for none (controlled). | string | null | — |
| defaultValue Initial reaction when uncontrolled. | string | null | null |
| onValueChange Called with the new key, or `null` when the person picks their current reaction again to remove it. | (key: string | null) => void | — |
| count Everyone's reactions on this item. Shown in the badge when above 1. | number | — |
| badgePlacement Badge corner under the element. `trailing` suits received messages and posts, `leading` sent messages. | "leading" | "trailing" | "trailing" |
| showBadge Show the chosen reaction as a badge on the element's bottom edge. Default `true`. | boolean | true |
| onPress A normal tap on the element. | () => void | — |
| onOpenChange Called when the pill opens or closes. | (open: boolean) => void | — |
| delayLongPress Hold duration in milliseconds before the pill opens. Default 300. | number | 300 |
| previewStyle Shape of the lifted copy. Match the element's own radius; default `{ borderRadius: 20 }`. | StyleProp<ViewStyle> | — |
| haptic Haptics on open, while sliding across reactions, and on the pick. Default `true`. | boolean | true |
| disabled Dims the control and ignores presses. | boolean | false |
| accessibilityLabel Screen reader label for content without text, such as a photo. Text content names itself (required on the web, where a label must contain the visible text). | string | — |
| labels Overrides for the built-in strings (default reaction names and screen reader text); a GooduiLocaleProvider pack sets them app-wide. | Partial<ReactionPickerLabels> | — |
| style Style for the outer container. | StyleProp<ViewStyle> | — |
| testID Identifier for end-to-end tests. | string | — |
Motion and haptics
- While held the element squeezes to 0.96, then a copy lifts slightly above a dimmed backdrop.
- The pill grows out of the element's side on the bouncy spring and each emoji springs in 28 ms after the one before it.
- Sliding: magnification is computed on the UI thread from the finger's distance to each emoji (up to 1.6 times and 16 pt up), easing in as the finger enters the pill's band; the hovered emoji shows its name above it.
- On release the chosen emoji flies along an arc to the badge in 460 ms, shrinking to badge size; the badge, hidden until then, pops with the bouncy spring. The space for the badge opens under the element on the snappy spring.
- Reduced motion: no squeeze, lift, stagger or magnification (the hovered emoji grows a little to show focus); the pill fades in, the emoji does not fly and the badge fades in.
- Haptic: tap when the pill opens
- Haptic: select for each emoji the finger slides onto, and on the pick (haptic={false} turns them off)
- Hit-testing and magnification run in worklets against the measured pill; React re-renders on open, on the pick and when the flight lands.
- The overlay renders through Portal only while open.
Accessibility
- The element exposes one action per reaction ('React with Love', or 'Remove Love reaction' for the current one) plus 'Show reactions', so screen reader users react without the gesture.
- Its label gains 'you reacted with Love, 12 reactions' when there is a reaction (the badge itself is hidden from screen readers); picking and removing are announced.
- On the web a label must contain the visible text, so give accessibilityLabel only when the content has no text (a photo). Text content names the element there, and 'you reacted with Love' follows as visually hidden text.
- The pill is a menu of menu items, each 44 by 44 pt and labelled, with the current reaction marked selected.
- On the web the element is a focusable button with a popup: right-click, the context-menu key or Shift+F10 open the pill (Enter and Space too when there is no onPress), focus moves to the first emoji when opened from the keyboard, and Escape closes it. Android's back button closes it.
- The badge is decorative; its content is in the element's label.
Notes for AI
- value is a reaction key or null; onValueChange receives null when the person picks their current reaction again.
- count is the total shown in the badge (including the person's own); it only shows above 1.
- Keep children non-interactive and put a normal tap in onPress; the long press belongs to the picker.
- Match previewStyle's borderRadius to the element so the lifted copy has the same shape.
- badgePlacement 'leading' for sent messages, 'trailing' for received ones and posts. The badge overlaps the element's bottom edge and the picker adds room below for it.
- For chat use ChatBubble's reaction props rather than wrapping it yourself.
Related
Message bubbles that group into runs with one tail, carry a receipt that ticks forward (clock, a check that draws itself, two checks, both brightening when read) and spring up out of the composer; plus a TypingIndicator whose dots wave and a Composer whose mic morphs into a filled send arrow as soon as there is text.
Long-press an element and it lifts out of the page above a dimmed backdrop while a menu springs out beside it; keep the finger down, slide onto an item and release to choose. Right-click opens it on the web.
A like toggle with one moment of delight: the heart squashes and springs back filled, a ring expands and particles burst while the count rolls; it updates optimistically and rolls back if the request fails.