Reaction picker

Free · MITReact Native · Expo

Long-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.

View as Markdown
Try it on your phone
Scan to open this demo in your phone's browser and use it with your fingers.

Installation

$ npx shadcn@latest add https://goodui.dev/native/r/reaction-picker.json

Then 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

Use it for
  • 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.
Reach for something else
  • 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

PropTypeDefault
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 | nullnull
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`.
booleantrue
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.
number300
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`.
booleantrue
disabled
Dims the control and ignores presses.
booleanfalse
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.