npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

expo-native-emojis-popup

v1.1.4

Published

A fully native emoji reaction popup for React Native. Built with Swift (iOS) and Kotlin (Android) for 60+ FPS animations. Anchor-based positioning, long-press drag-to-select, haptic feedback, hover labels, and fully customizable styling.

Readme

expo-native-emojis-popup

npm version

A fully native emoji reaction popup for React Native. Built entirely in Swift (iOS) and Kotlin (Android) -- every interaction runs at 60+ FPS with zero bridge overhead.

| iOS | Android | |-|-| | | |

Highlights

  • Fully native on both platforms -- Swift (iOS) and Kotlin (Android), no JavaScript rendering or web views
  • 60+ FPS animations -- spring-based hover effects, staggered emoji entrances, and smooth drag interactions
  • Anchor-based positioning -- popup appears relative to a trigger element with smart placement
  • Long-press drag-to-select -- open with long press, drag over emojis to hover, release to select
  • Scrollable overflow -- when emojis exceed the popup width the row scrolls horizontally, clipped to the tray; finger-scroll in tap mode, drag-to-edge auto-scroll during long-press drag
  • Hover labels -- emoji names appear above hovered items during drag gestures
  • Haptic feedback -- configurable per-event (open, select, plus button)
  • Plus button -- optional "more reactions" button that pairs with expo-native-sheet-emojis for a complete reaction system (see Companion)
  • Selected state -- highlights the currently active reaction
  • Smart placement -- auto/above/below with safe area awareness and edge padding
  • Full style customization -- 30+ style properties covering colors, borders, shadows, and hover labels
  • Animation customization -- stagger timing, duration, scale, and initial tray scale
  • Accessibility -- VoiceOver/TalkBack support, respects reduce motion preferences
  • Two APIs -- imperative show()/dismiss() and declarative <EmojisPopup>

Installation

npx expo install expo-native-emojis-popup

For bare workflow projects, run npx expo prebuild after installation.

For bare React Native projects, you must ensure that you have installed and configured the expo package before continuing.

yarn add expo-native-emojis-popup
cd ios && pod install

Quick Start

The imperative API shows a native popup anchored to a view and returns the user's action:

import { EmojisPopupModule } from 'expo-native-emojis-popup';

async function showReactions() {
  const result = await EmojisPopupModule.show({
    anchorId: 'message-42',
    items: [
      { emoji: '❤️', emoji_name: 'Red Heart', id: 'heart' },
      { emoji: '👍', emoji_name: 'Thumbs Up', id: 'thumbsup' },
      { emoji: '😂', emoji_name: 'Face with Tears of Joy', id: 'laugh' },
      { emoji: '🔥', emoji_name: 'Fire', id: 'fire' },
      { emoji: '😢', emoji_name: 'Crying Face', id: 'sad' },
    ],
    plusEnabled: true,
    selectedId: 'heart',
  });

  switch (result.type) {
    case 'select':
      console.log('Selected reaction:', result.id);
      break;
    case 'plus':
      console.log('Open full emoji picker');
      break;
    case 'dismiss':
      console.log('Popup dismissed');
      break;
  }
}

Declarative Usage

Wrap a trigger element with EmojisPopup for gesture-driven interaction:

import { EmojisPopup } from 'expo-native-emojis-popup';
import type { ShowReactionPopupParams } from 'expo-native-emojis-popup';

function MessageBubble({ message }) {
  const anchorId = `message-${message.id}`;
  
  const dragParams: Omit<ShowReactionPopupParams, 'onOpen' | 'onClose'> = {
    anchorId,
    haptics: { onOpen: true, onSelect: true },
    items: [
      { emoji: '❤️', emoji_name: 'Red Heart', id: 'heart' },
      { emoji: '👍', emoji_name: 'Thumbs Up', id: 'thumbsup' },
      { emoji: '😂', emoji_name: 'Face with Tears of Joy', id: 'laugh' },
    ],
    plusEnabled: true,
    selectedId: message.currentReaction,
  };

  return (
    <EmojisPopup
      anchorId={anchorId}
      dragParams={dragParams}
      gestureMode="longPressDrag"
      onDragDismiss={() => console.log('Dismissed')}
      onDragPlus={() => console.log('Open full picker')}
      onDragSelect={(event) => console.log('Selected:', event.nativeEvent.id)}
    >
      <Text>{message.text}</Text>
    </EmojisPopup>
  );
}

Exports

import { EmojisPopup, EmojisPopupModule } from 'expo-native-emojis-popup';
import type {
  DragDismissEvent,
  DragPlusEvent,
  DragSelectEvent,
  EmojisPopupModuleType,
  EmojisPopupProps,
  NativeReactionPopupAnimation,
  NativeReactionPopupErrorCode,
  NativeReactionPopupHaptics,
  NativeReactionPopupItem,
  NativeReactionPopupPlacement,
  NativeReactionPopupResult,
  NativeReactionPopupStyle,
  ShowReactionPopupParams,
  TapEvent,
} from 'expo-native-emojis-popup';

TypeScript Types

NativeReactionPopupItem

type NativeReactionPopupItem = {
  emoji: string;
  emoji_name: string;
  id: string;
};

NativeReactionPopupStyle

type NativeReactionPopupStyle = {
  backdropColor?: string;
  backdropOpacity?: number;
  backgroundColor?: string;
  borderColor?: string;
  borderRadius?: number;
  borderWidth?: number;
  elevation?: number;
  emojiFontSize?: number;
  emojiPopScale?: number;
  gap?: number;
  hoverLabelBackgroundColor?: string;
  hoverLabelBorderRadius?: number;
  hoverLabelColor?: string;
  hoverLabelFontSize?: number;
  hoverLabelPaddingHorizontal?: number;
  hoverLabelPaddingVertical?: number;
  hoverScale?: number;
  hoverTranslationY?: number;
  itemBackgroundColor?: string;
  itemBorderColor?: string;
  itemBorderRadius?: number;
  itemBorderWidth?: number;
  itemPressedBackgroundColor?: string;
  itemSelectedBackgroundColor?: string;
  itemSize?: number;
  paddingHorizontal?: number;
  paddingVertical?: number;
  plusBackgroundColor?: string;
  plusIconColor?: string;
  plusPressedBackgroundColor?: string;
  shadowColor?: string;
  shadowOffsetX?: number;
  shadowOffsetY?: number;
  shadowOpacity?: number;
  shadowRadius?: number;
};

ShowReactionPopupParams

type ShowReactionPopupParams = {
  anchorId: string;
  animation?: {
    emojiPopScale?: number;
    itemStaggerMs?: number;
    openDurationMs?: number;
    trayInitialScale?: number;
  };
  centerOnScreen?: boolean;
  dismissOnBackdropPress?: boolean;
  dismissOnDragOut?: boolean;
  edgePadding?: number;
  haptics?: {
    onOpen?: boolean;
    onPlus?: boolean;
    onSelect?: boolean;
  };
  items: NativeReactionPopupItem[];
  onClose?: (result: NativeReactionPopupCloseResult) => void;
  onOpen?: () => void;
  plusAccessibilityLabel?: string;
  plusEnabled?: boolean;
  preferredPlacement?: 'above' | 'below' | 'auto';
  selectedId?: string | null;
  showLabels?: boolean;
  hideLabelsInSafeArea?: boolean;
  style?: NativeReactionPopupStyle;
};

NativeReactionPopupResult

type NativeReactionPopupResult =
  | { type: 'select'; id: string }
  | { type: 'plus' }
  | { type: 'dismiss' };

NativeReactionPopupCloseResult

Extended result type with a cancelled boolean, passed to the onClose callback:

type NativeReactionPopupCloseResult =
  | { type: 'select'; id: string; cancelled: false }
  | { type: 'plus'; cancelled: false }
  | { type: 'dismiss'; cancelled: true };

NativeReactionPopupErrorCode

type NativeReactionPopupErrorCode =
  | 'ANCHOR_NOT_FOUND'
  | 'ANCHOR_NOT_MEASURABLE'
  | 'EMPTY_ITEMS'
  | 'INVALID_PARAMS'
  | 'PRESENTATION_FAILED';

API Reference

EmojisPopupModule.show(params)

Presents the reaction popup anchored to a native view. Returns a promise that resolves when the user selects a reaction, taps plus, or dismisses the popup.

Parameters:

| Name | Type | Description | |-|-|-| | params | ShowReactionPopupParams | Configuration object (see below) |

Returns: Promise<NativeReactionPopupResult>

The result is a discriminated union:

  • { type: 'select', id: string } when a reaction is selected
  • { type: 'plus' } when the plus button is tapped
  • { type: 'dismiss' } when the popup is dismissed without selection

ShowReactionPopupParams fields:

| Field | Type | Default | Description | |-|-|-|-| | anchorId | string | required | ID of the anchor view to position the popup relative to | | animation | NativeReactionPopupAnimation | -- | Animation timing configuration | | centerOnScreen | boolean | false | Center popup on screen instead of anchoring | | dismissOnBackdropPress | boolean | true | Dismiss when tapping outside the popup | | dismissOnDragOut | boolean | false | Dismiss when drag is released outside the popup (by default the popup stays open and converts to tap mode) | | edgePadding | number | 12 | Minimum distance from screen edges | | haptics | NativeReactionPopupHaptics | all false | Haptic feedback configuration | | items | NativeReactionPopupItem[] | required | Array of emoji items to display | | plusAccessibilityLabel | string | 'More reactions' | Accessibility label for the plus button | | plusEnabled | boolean | false | Show the plus button for "more reactions" (see Companion: expo-native-sheet-emojis) | | preferredPlacement | 'above' \| 'below' \| 'auto' | 'auto' | Preferred popup position relative to anchor | | selectedId | string \| null | null | ID of the currently selected reaction | | showLabels | boolean | true | Show emoji name labels on hover during drag gestures | | hideLabelsInSafeArea | boolean | true | Hide hover labels when they would overlap the safe area (notch/status bar). Useful when the popup appears near the top of the screen and the scaled emoji + label would clip under the notch | | style | NativeReactionPopupStyle | -- | Visual style overrides |

EmojisPopupModule.dismiss()

Programmatically dismisses the reaction popup.

Returns: Promise<void>

EmojisPopup

A declarative React component that wraps a trigger element and manages popup presentation via gestures.

Props:

| Prop | Type | Default | Description | |-|-|-|-| | anchorId | string | required | Unique identifier for this wrapper view | | children | React.ReactElement | required | Trigger element to wrap | | dragParams | Omit<ShowReactionPopupParams, 'onOpen' \| 'onClose'> | -- | Popup configuration for drag gesture mode | | gestureMode | 'none' \| 'longPressDrag' | 'none' | Gesture handling mode | | onDragDismiss | (event: DragDismissEvent) => void | -- | Called when drag ends without selection | | onDragPlus | (event: DragPlusEvent) => void | -- | Called when plus button is reached via drag | | onDragSelect | (event: DragSelectEvent) => void | -- | Called when a reaction is selected via drag | | onTap | (event: TapEvent) => void | -- | Called when the user taps (press shorter than long-press threshold) in longPressDrag mode. Use this to open the modal popup via EmojisPopupModule.show() so the same button supports both interactions |

Gesture Modes

The gestureMode prop on EmojisPopup controls how the popup is triggered:

  • none (default) -- No gesture handling. Use with the imperative API (EmojisPopupModule.show()) for full control over when the popup appears.
  • longPressDrag -- Long press opens the popup, then drag your finger over emojis to hover and preview. Releasing your finger over an emoji selects it. Releasing outside the popup keeps it open and converts to tap mode (set dismissOnDragOut: true to dismiss instead). Hover labels showing the emoji name appear above each item during drag. If the emojis overflow the popup width, dragging to either edge auto-scrolls the row so every emoji stays reachable without lifting your finger.

Scrolling & overflow: With more emojis than fit the popup width, the row scrolls horizontally and is clipped to the tray bounds. In tap mode (show()) the user finger-scrolls the row; in longPressDrag mode the drag-to-edge auto-scroll above keeps every item reachable. The popup width is capped to the screen minus edgePadding.

Dual Mode (Tap + Long-Press Drag)

Some UIs benefit from both interactions on the same button: a quick tap opens the modal popup instantly, while a long-press-drag lets users select without lifting their finger. Use gestureMode="longPressDrag" together with onTap to achieve this cleanly.

onTap is fired natively (from the gesture recognizer) when the user releases before the long-press threshold — no JS timing hacks required.

import { EmojisPopup, EmojisPopupModule } from 'expo-native-emojis-popup';

function ReactionButton({ postId, currentReaction }) {
  const anchorId = `post:${postId}:react`;

  const handleTap = async () => {
    // Short tap — open the modal popup via the imperative API
    const result = await EmojisPopupModule.show({
      anchorId,
      items: REACTIONS,
      selectedId: currentReaction,
      preferredPlacement: 'above',
      haptics: { onOpen: true, onSelect: true },
    });
    if (result.type === 'select') onReact(result.id);
  };

  return (
    <EmojisPopup
      anchorId={anchorId}
      dragParams={{ anchorId, items: REACTIONS, selectedId: currentReaction, preferredPlacement: 'above' }}
      gestureMode="longPressDrag"
      onDragSelect={(e) => onReact(e.nativeEvent.id)}
      onDragDismiss={() => {}}
      onTap={handleTap}
    >
      <Pressable>
        <Text>React</Text>
      </Pressable>
    </EmojisPopup>
  );
}

Placement

The preferredPlacement option controls where the popup appears relative to the anchor view:

  • auto (default) -- Prefers placing the popup above the anchor. Falls back to below if there is not enough space above.
  • above -- Always positions above the anchor if space permits.
  • below -- Always positions below the anchor if space permits.

All placement modes respect safe area insets and the edgePadding value to ensure the popup stays within visible bounds.

Style Presets

Facebook-like Dark

const facebookDarkStyle: NativeReactionPopupStyle = {
  backdropColor: '#000000',
  backdropOpacity: 0.4,
  backgroundColor: '#242526',
  borderRadius: 28,
  elevation: 8,
  emojiFontSize: 28,
  gap: 4,
  hoverLabelBackgroundColor: '#242526',
  hoverLabelBorderRadius: 8,
  hoverLabelColor: '#E4E6EB',
  hoverLabelFontSize: 12,
  hoverLabelPaddingHorizontal: 8,
  hoverLabelPaddingVertical: 4,
  hoverScale: 1.4,
  hoverTranslationY: -8,
  itemBorderRadius: 22,
  itemPressedBackgroundColor: '#3A3B3C',
  itemSelectedBackgroundColor: '#3A3B3C',
  itemSize: 44,
  paddingHorizontal: 8,
  paddingVertical: 6,
  plusBackgroundColor: '#3A3B3C',
  plusIconColor: '#B0B3B8',
  plusPressedBackgroundColor: '#4E4F50',
  shadowColor: '#000000',
  shadowOffsetY: 4,
  shadowOpacity: 0.3,
  shadowRadius: 12,
};

Light Minimal

const lightMinimalStyle: NativeReactionPopupStyle = {
  backdropColor: '#000000',
  backdropOpacity: 0.15,
  backgroundColor: '#FFFFFF',
  borderRadius: 24,
  elevation: 4,
  emojiFontSize: 26,
  gap: 2,
  hoverLabelBackgroundColor: '#333333',
  hoverLabelBorderRadius: 6,
  hoverLabelColor: '#FFFFFF',
  hoverLabelFontSize: 11,
  hoverLabelPaddingHorizontal: 6,
  hoverLabelPaddingVertical: 3,
  hoverScale: 1.3,
  hoverTranslationY: -6,
  itemBorderRadius: 20,
  itemPressedBackgroundColor: '#F0F0F0',
  itemSelectedBackgroundColor: '#E8F0FE',
  itemSize: 40,
  paddingHorizontal: 6,
  paddingVertical: 6,
  plusBackgroundColor: '#F5F5F5',
  plusIconColor: '#9E9E9E',
  plusPressedBackgroundColor: '#E0E0E0',
  shadowColor: '#000000',
  shadowOffsetY: 2,
  shadowOpacity: 0.1,
  shadowRadius: 8,
};

Custom Hook Pattern

For apps that use the reaction popup in multiple places, extract a reusable hook to centralize configuration:

import { EmojisPopupModule } from 'expo-native-emojis-popup';
import type {
  NativeReactionPopupItem,
  NativeReactionPopupResult,
  ShowReactionPopupParams,
} from 'expo-native-emojis-popup';

const DEFAULT_REACTIONS: NativeReactionPopupItem[] = [
  { emoji: '❤️', emoji_name: 'Red Heart', id: 'heart' },
  { emoji: '👍', emoji_name: 'Thumbs Up', id: 'thumbsup' },
  { emoji: '😂', emoji_name: 'Face with Tears of Joy', id: 'laugh' },
  { emoji: '😮', emoji_name: 'Face with Open Mouth', id: 'surprise' },
  { emoji: '😢', emoji_name: 'Crying Face', id: 'sad' },
  { emoji: '🔥', emoji_name: 'Fire', id: 'fire' },
];

export function useReactionPopup() {
  const theme = useAppTheme();

  const show = async (
    anchorId: string,
    selectedId?: string | null,
    overrides?: Partial<ShowReactionPopupParams>
  ): Promise<NativeReactionPopupResult> => {
    return EmojisPopupModule.show({
      anchorId,
      items: DEFAULT_REACTIONS,
      plusEnabled: true,
      selectedId,
      style: {
        backgroundColor: theme.colors.surface,
        hoverLabelBackgroundColor: theme.colors.tooltip,
        hoverLabelColor: theme.colors.tooltipText,
        itemSelectedBackgroundColor: theme.colors.primaryLight,
        shadowColor: theme.colors.shadow,
      },
      ...overrides,
    });
  };

  return { show };
}

Then use it anywhere:

const reactionPopup = useReactionPopup();

const result = await reactionPopup.show('message-42', currentReactionId);
if (result.type === 'select') {
  toggleReaction(result.id);
} else if (result.type === 'plus') {
  openFullEmojiPicker();
}

Error Codes

When EmojisPopupModule.show() rejects, the error includes a code property:

| Code | Description | |-|-| | ANCHOR_NOT_FOUND | No native view found with the given anchorId | | ANCHOR_NOT_MEASURABLE | Anchor view exists but could not be measured (not yet laid out) | | EMPTY_ITEMS | The items array is empty | | INVALID_PARAMS | Parameters failed validation (e.g., missing required fields) | | PRESENTATION_FAILED | Native presentation failed (e.g., another popup is already visible) |

Companion: expo-native-sheet-emojis

The plus button is designed to open a full emoji picker for extended reactions beyond the quick-select tray. The companion module expo-native-sheet-emojis provides exactly this -- a fully native emoji picker bottom sheet with 1900+ emojis, search across 21 languages, skin tone selection, and configurable theming.

Together they form a complete reaction system:

  1. User long-presses a message/post -> expo-native-emojis-popup shows the quick reaction tray
  2. User taps the plus button -> your app presents expo-native-sheet-emojis for the full emoji catalog
  3. Selected emoji flows back into your reaction system
import { EmojisPopupModule } from 'expo-native-emojis-popup';
import { EmojiSheetModule } from 'expo-native-sheet-emojis';

const result = await EmojisPopupModule.show({
  anchorId: 'message:42',
  items: quickReactions,
  plusEnabled: true,
});

if (result.type === 'plus') {
  // Open the full emoji picker
  const sheetResult = await EmojiSheetModule.present({ theme: 'dark' });
  if (!sheetResult.cancelled) {
    handleReaction(sheetResult.emoji);
  }
} else if (result.type === 'select') {
  handleReaction(result.id);
}

Related Projects

  • expo-native-sheet-emojis -- Companion full emoji picker bottom sheet (1900+ emojis, multilingual search, skin tones, theming)

LLM / AI Agent Reference

If you're an AI agent or using an LLM to integrate this module, see llms.txt for a concise, structured reference with all types, APIs, and usage patterns.

Contributing

Contributions are welcome! Please read the contributing guide before submitting a pull request.

License

MIT