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 🙏

© 2025 – Pkg Stats / Ryan Hefner

@shaquillehinds/react-native-menu-modal

v0.0.8

Published

A simple location/spot modal for react native that just works.

Readme

@shaquillehinds/react-native-menu-modal

npm version License: MIT

A simple, elegant context menu modal for React Native that appears at the touch location. Perfect for creating contextual menus, dropdown options, and interactive lists triggered by press or long-press gestures.

Features

  • 🎯 Position-aware - Opens at exact touch coordinates
  • Flexible activation - Trigger on press or long-press
  • 🎨 Highly customizable - Style every aspect of the menu
  • 📱 Cross-platform - Works seamlessly on iOS and Android
  • 🔧 TypeScript support - Fully typed for excellent DX
  • 🚀 Performant - Built with react-native-reanimated and gesture-handler
  • 🎭 Portal support - Render menus in a portal layer or native modal
  • Accessible - Supports subtitles and custom components
  • 🎁 Zero config - Works out of the box, no provider required!

Quick Start

import { MenuModal } from '@shaquillehinds/react-native-menu-modal';

// That's it! Works immediately without any setup
<MenuModal
  backgroundColor="#1a1a1a"
  options={[
    { title: 'Edit', onOptionPress: () => console.log('Edit') },
    { title: 'Delete', onOptionPress: () => console.log('Delete') },
  ]}
>
  <Text>Press me!</Text>
</MenuModal>;

Installation

npm install @shaquillehinds/react-native-menu-modal

or

yarn add @shaquillehinds/react-native-menu-modal

Peer Dependencies

This package requires the following peer dependencies:

npm install @shaquillehinds/react-native-spot-modal react-native-reanimated react-native-gesture-handler

or

yarn add @shaquillehinds/react-native-spot-modal react-native-reanimated react-native-gesture-handler

Note: Make sure to complete the setup for react-native-reanimated and react-native-gesture-handler.

Portal Provider Setup (Optional)

The MenuModalPortalProvider is optional. The menu modal works out of the box without it by falling back to React Native's Modal component. However, the portal provider offers better control and rendering flexibility.

Quick Start (No Provider Needed):

import { MenuModal } from '@shaquillehinds/react-native-menu-modal';

// Works immediately - uses React Native Modal as fallback
export default function App() {
  return (
    <MenuModal
      backgroundColor="#1a1a1a"
      options={[
        { title: 'Option 1', onOptionPress: () => console.log('1') },
        { title: 'Option 2', onOptionPress: () => console.log('2') },
      ]}
    >
      <Text>Press me</Text>
    </MenuModal>
  );
}

With Portal Provider (Recommended for Better Control):

If you want more control over rendering layers, add the provider at the root of your application:

import { MenuModalPortalProvider } from '@shaquillehinds/react-native-menu-modal';

export default function App() {
  return (
    <MenuModalPortalProvider>{/* Your app content */}</MenuModalPortalProvider>
  );
}

For Expo Router:

// app/_layout.tsx
import { MenuModalPortalProvider } from '@shaquillehinds/react-native-menu-modal';
import { Stack } from 'expo-router';

export default function RootLayout() {
  return (
    <MenuModalPortalProvider>
      <Stack />
    </MenuModalPortalProvider>
  );
}

For React Navigation:

import { NavigationContainer } from '@react-navigation/native';
import { MenuModalPortalProvider } from '@shaquillehinds/react-native-menu-modal';

export default function App() {
  return (
    <MenuModalPortalProvider>
      <NavigationContainer>{/* Your navigators */}</NavigationContainer>
    </MenuModalPortalProvider>
  );
}

Note: When using the portal provider, it should be placed as high as possible in your component tree, typically wrapping your navigation or root component.

Usage

Basic Example

import { MenuModal } from '@shaquillehinds/react-native-menu-modal';
import { Text, View } from 'react-native';

export default function App() {
  return (
    <View style={{ flex: 1, justifyContent: 'center', alignItems: 'center' }}>
      <MenuModal
        backgroundColor="#1a1a1a"
        options={[
          {
            id: 'edit',
            title: 'Edit',
            onOptionPress: ({ title }) => console.log(`${title} pressed`),
          },
          {
            id: 'share',
            title: 'Share',
            onOptionPress: () => console.log('Share pressed'),
          },
          {
            id: 'delete',
            title: 'Delete',
            subTitle: 'Cannot be undone',
            onOptionPress: () => console.log('Delete pressed'),
          },
        ]}
      >
        <Text>Press me for menu</Text>
      </MenuModal>
    </View>
  );
}

Long Press Activation

<MenuModal
  backgroundColor="#1a1a1a"
  activateOn="long-press"
  options={[
    {
      title: 'Copy',
      onOptionPress: () => console.log('Copied!'),
    },
    {
      title: 'Paste',
      onOptionPress: () => console.log('Pasted!'),
    },
  ]}
>
  <Text>Long press me</Text>
</MenuModal>

With Icons and Custom Components

import { Feather } from '@expo/vector-icons';

<MenuModal
  backgroundColor="#ffffff"
  options={[
    {
      id: 'download',
      title: 'Download',
      subTitle: 'Save to device',
      leftComponent: (
        <Feather
          name="download"
          size={20}
          color="#000"
          style={{ marginRight: 12 }}
        />
      ),
      onOptionPress: () => console.log('Download'),
    },
    {
      id: 'star',
      title: 'Add to Favorites',
      leftComponent: (
        <Feather
          name="star"
          size={20}
          color="#FFD700"
          style={{ marginRight: 12 }}
        />
      ),
      rightComponent: (
        <Text style={{ marginLeft: 'auto', color: '#888' }}>⌘F</Text>
      ),
      onOptionPress: () => console.log('Favorited'),
    },
  ]}
>
  <Text>Open Menu</Text>
</MenuModal>;

Keeping Menu Open After Selection

<MenuModal
  backgroundColor="#2a2a2a"
  disableDismissOnPress
  options={[
    {
      title: 'Option 1',
      disableDismissOnOptionPress: true,
      onOptionPress: ({ dismiss }) => {
        console.log('Do something...');
        // Manually dismiss when ready
        setTimeout(() => dismiss(), 1000);
      },
    },
    {
      title: 'Option 2',
      onOptionPress: () => console.log('Auto dismisses'),
    },
  ]}
>
  <Text>Options</Text>
</MenuModal>

Imperative Control with Ref

import { useRef } from 'react';
import type { MenuModalRef } from '@shaquillehinds/react-native-menu-modal';

function MyComponent() {
  const menuRef = useRef<MenuModalRef>(null);

  const openMenu = () => {
    // Open menu at specific coordinates
    menuRef.current?.open({ pageX: 100, pageY: 200 });
  };

  const closeMenu = () => {
    menuRef.current?.close();
  };

  return (
    <>
      <Button title="Open Menu" onPress={openMenu} />
      <MenuModal
        refMenuModal={menuRef}
        backgroundColor="#1a1a1a"
        options={[
          {
            title: 'Close',
            onOptionPress: closeMenu,
          },
        ]}
      >
        <Text>Child (won't trigger menu, use ref instead)</Text>
      </MenuModal>
    </>
  );
}

Scrollable Menu

<MenuModal
  backgroundColor="#ffffff"
  scrollable
  maxHeight={300}
  options={Array.from({ length: 20 }, (_, i) => ({
    id: `option-${i}`,
    title: `Option ${i + 1}`,
    subTitle: `Description for option ${i + 1}`,
    onOptionPress: ({ title }) => console.log(title),
  }))}
>
  <Text>Many Options</Text>
</MenuModal>

Custom Styling

<MenuModal
  backgroundColor="#1e1e1e"
  padding={[10, 15]}
  borderRadius={20}
  separatorStyle={{
    backgroundColor: '#444',
    height: 2,
    marginVertical: 8,
  }}
  options={[
    {
      title: 'Custom Styled',
      subTitle: 'With custom padding',
      padding={[12, 16]},
      titleProps: {
        fontSize: 'titleL',
        customColor: '#ffffff',
      },
      subTitleProps: {
        fontSize: 'bodyS',
        customColor: '#999999',
      },
      onOptionPress: () => console.log('Styled!'),
    },
  ]}
>
  <Text>Styled Menu</Text>
</MenuModal>

Using Native Modal or Portal

// Use native modal (default behavior via SpotModal)
<MenuModal
  backgroundColor="#ffffff"
  disableNativeModal={false}
  options={[...]}
>
  <Text>Native Modal</Text>
</MenuModal>

// Disable portal rendering (renders inline)
<MenuModal
  backgroundColor="#ffffff"
  disablePortal={true}
  options={[...]}
>
  <Text>Inline Modal</Text>
</MenuModal>

API Reference

MenuModal Props

| Prop | Type | Default | Description | | ----------------------- | ---------------------------------------------- | ------------ | -------------------------------------------------------------------------------------------------- | | children | ReactNode | required | The component that triggers the menu | | options | MenuOption[] | required | Array of menu options | | backgroundColor | string | required | Background color of the menu | | activateOn | 'press' \| 'long-press' | 'press' | How to trigger the menu | | onPress | () => void \| Promise<void> | undefined | Callback when trigger is pressed | | onLongPress | () => void \| Promise<void> | undefined | Callback when trigger is long-pressed | | separatorStyle | ViewStyle | undefined | Custom style for option separators | | disableDismissOnPress | boolean | false | Prevent menu from dismissing on option press | | disablePortal | boolean | false | Disable portal rendering (even if provider exists). Falls back to native modal or inline rendering | | disableNativeModal | boolean | false | Disable native Modal component. Only use portal (if provider exists) or inline rendering | | refMenuModal | React.MutableRefObject<MenuModalRef \| null> | undefined | Ref for imperative control | | scrollable | boolean | false | Enable scrolling for long menus | | maxHeight | number | undefined | Maximum height when scrollable | | padding | number \| [number, number] | [2, 5] | Menu padding | | borderRadius | number | 15 | Menu border radius | | ...LayoutProps | LayoutProps | - | Additional layout props from @shaquillehinds/react-native-essentials |

MenuOption Interface

| Property | Type | Description | | ----------------------------- | ------------------------------------------------- | ------------------------------------------------- | | id | string | Unique identifier for the option | | title | string | required - Main text of the option | | subTitle | string | Optional subtitle text | | leftComponent | ReactNode | Component to render on the left (e.g., icon) | | rightComponent | ReactNode | Component to render on the right (e.g., shortcut) | | onOptionPress | (info: MenuOptionInfo) => void \| Promise<void> | Handler for press events | | onOptionLongPress | (info: MenuOptionInfo) => void \| Promise<void> | Handler for long-press events | | disableDismissOnOptionPress | boolean | Prevent dismissal when this option is pressed | | titleProps | MenuOptionTitleTextProps | Custom props for title text | | subTitleProps | MenuOptionBodyTextProps | Custom props for subtitle text | | ...LayoutProps | LayoutProps | Additional layout props for the option container |

MenuOptionInfo

The callback parameter passed to onOptionPress and onOptionLongPress:

{
  id?: string;
  title: string;
  subTitle?: string;
  index: number;
  dismiss: () => void;
}

MenuModalRef Methods

| Method | Parameters | Description | | ------- | ---------------------------------- | -------------------------------------- | | open | { pageX: number, pageY: number } | Opens the menu at specific coordinates | | close | - | Closes the menu |

Text Styling Props

MenuOptionTitleTextProps

Extends BaseTextProps from @shaquillehinds/react-native-essentials with:

  • fontSize?: 'titleS' | 'titleL'

MenuOptionBodyTextProps

Extends BaseTextProps from @shaquillehinds/react-native-essentials with:

  • fontSize?: 'bodyS' | 'bodyL'

Advanced Usage

Using Portal Hooks

The package exports portal utilities from @shaquillehinds/react-native-spot-modal for advanced use cases:

import {
  useMenuModalPortal,
  useMenuModalPortalComponent,
  type PortalItem,
} from '@shaquillehinds/react-native-menu-modal';

function MyComponent() {
  // Access the portal API directly
  const portal = useMenuModalPortal();

  // Add a component to the portal
  const addToPortal = () => {
    const portalItem: PortalItem = {
      id: 'my-custom-overlay',
      component: <MyCustomOverlay />,
    };
    portal.add(portalItem);
  };

  // Remove from portal
  const removeFromPortal = () => {
    portal.remove('my-custom-overlay');
  };

  return (
    <View>
      <Button title="Show Overlay" onPress={addToPortal} />
      <Button title="Hide Overlay" onPress={removeFromPortal} />
    </View>
  );
}

Using useMenuModalPortalComponent for Reusable Portal Components:

import { useMenuModalPortalComponent } from '@shaquillehinds/react-native-menu-modal';

function MyPortalComponent() {
  // Automatically manages adding/removing from portal
  useMenuModalPortalComponent({
    id: 'notification-toast',
    component: (
      <View style={styles.toast}>
        <Text>This is rendered in the portal!</Text>
      </View>
    ),
  });

  return null; // This component doesn't render anything directly
}

// Use it anywhere in your app
function App() {
  const [showToast, setShowToast] = useState(false);

  return (
    <MenuModalPortalProvider>
      {showToast && <MyPortalComponent />}
      <Button title="Show Toast" onPress={() => setShowToast(true)} />
    </MenuModalPortalProvider>
  );
}

Note: These hooks are useful for custom overlay components or when you need more control over portal rendering. For most menu use cases, the standard MenuModal component handles portal management automatically.

Dynamic Options

const [options, setOptions] = useState([
  { title: 'Option 1', id: '1', onOptionPress: () => console.log('1') },
  { title: 'Option 2', id: '2', onOptionPress: () => console.log('2') },
]);

const addOption = () => {
  setOptions((prev) => [
    ...prev,
    {
      title: `Option ${prev.length + 1}`,
      id: `${prev.length + 1}`,
      onOptionPress: () => console.log(prev.length + 1),
    },
  ]);
};

return (
  <>
    <Button title="Add Option" onPress={addOption} />
    <MenuModal backgroundColor="#1a1a1a" options={options}>
      <Text>Dynamic Menu</Text>
    </MenuModal>
  </>
);

Async Operations

<MenuModal
  backgroundColor="#2a2a2a"
  options={[
    {
      title: 'Save',
      onOptionPress: async ({ dismiss }) => {
        // Perform async operation
        await saveData();
        // Dismiss after completion
        dismiss();
      },
    },
  ]}
>
  <Text>Save Menu</Text>
</MenuModal>

Conditional Options

const isAdmin = true;

<MenuModal
  backgroundColor="#ffffff"
  options={[
    {
      title: 'View',
      onOptionPress: () => console.log('View'),
    },
    ...(isAdmin
      ? [
          {
            title: 'Edit',
            onOptionPress: () => console.log('Edit'),
          },
          {
            title: 'Delete',
            onOptionPress: () => console.log('Delete'),
          },
        ]
      : []),
  ]}
>
  <Text>Conditional Menu</Text>
</MenuModal>;

Nested Information

<MenuModal
  backgroundColor="#1a1a1a"
  options={[
    {
      title: 'File Size',
      subTitle: '2.4 MB',
      leftComponent: <Feather name="file" size={18} color="#888" />,
      rightComponent: <Text style={{ color: '#666', fontSize: 12 }}>PDF</Text>,
      onOptionPress: () => console.log('File info'),
    },
    {
      title: 'Last Modified',
      subTitle: 'December 10, 2024',
      leftComponent: <Feather name="clock" size={18} color="#888" />,
      onOptionPress: () => console.log('Date info'),
    },
  ]}
>
  <Text>Document Info</Text>
</MenuModal>

Tips & Best Practices

Performance

  • Use id prop on options for optimal React rendering
  • Memoize option handlers if they're complex
  • Consider using disableDismissOnPress for multi-step operations
const handleEdit = useCallback(({ title, dismiss }) => {
  console.log(`Editing ${title}`);
  dismiss();
}, []);

const options = useMemo(
  () => [{ id: 'edit', title: 'Edit', onOptionPress: handleEdit }],
  [handleEdit]
);

Accessibility

  • Always provide descriptive titles
  • Use subtitles for additional context
  • Consider adding icons for visual clarity
  • Test with screen readers

Styling

  • Use consistent backgroundColor across your app
  • Match your app's design system with titleProps and subTitleProps
  • Customize separatorStyle to match your theme
  • Consider dark/light mode support
const isDark = useColorScheme() === 'dark';

<MenuModal
  backgroundColor={isDark ? '#1a1a1a' : '#ffffff'}
  options={options.map((opt) => ({
    ...opt,
    titleProps: {
      customColor: isDark ? '#ffffff' : '#000000',
    },
  }))}
>
  <Text>Theme-aware Menu</Text>
</MenuModal>;

Rendering Modes & Portal Usage

The menu modal supports multiple rendering modes with automatic fallback:

Default Behavior (Automatic Fallback Chain):

  1. With Portal Provider → Uses portal layer (best option)
  2. Without Portal Provider → Falls back to React Native's Modal component
  3. Both work perfectly fine out of the box!
// Works without any provider - uses React Native Modal
<MenuModal
  backgroundColor="#1a1a1a"
  options={[...]}
>
  <Text>Works Immediately!</Text>
</MenuModal>

// With provider - uses portal layer
<MenuModalPortalProvider>
  <MenuModal
    backgroundColor="#1a1a1a"
    options={[...]}
  >
    <Text>Uses Portal Layer</Text>
  </MenuModal>
</MenuModalPortalProvider>

Disabling Native Modal (forces portal or inline):

// If you have a portal provider but want to disable the native modal fallback
<MenuModal
  backgroundColor="#1a1a1a"
  disableNativeModal={true}
  options={[...]}
>
  <Text>Portal Only (no native modal fallback)</Text>
</MenuModal>

Disabling Portal (forces native modal or inline):

// Even with a portal provider present, opt-out to use native modal
<MenuModalPortalProvider>
  <MenuModal
    backgroundColor="#1a1a1a"
    disablePortal={true}
    options={[...]}
  >
    <Text>Skips Portal, Uses Native Modal</Text>
  </MenuModal>
</MenuModalPortalProvider>

Disabling Both (inline rendering only):

// Renders within parent container bounds - useful for specific layouts
<MenuModal
  backgroundColor="#1a1a1a"
  disablePortal={true}
  disableNativeModal={true}
  options={[...]}
>
  <Text>Renders Inline (respects parent overflow/z-index)</Text>
</MenuModal>

When to Use Each Mode:

| Mode | Use Case | | ------------------------------------------------------- | ---------------------------------------------------------------------------------------------- | | Portal (default with provider) | Best for most cases - renders above all content with proper layering | | Native Modal (default without provider) | Automatic fallback - works everywhere, good for full-screen menus | | Inline (disablePortal + disableNativeModal) | When you need the menu constrained within a specific container or scrollable area | | Portal disabled (disablePortal={true}) | Opt out of portal when you have the provider but want native modal behavior for specific menus | | Native Modal disabled (disableNativeModal={true}) | Force portal-only rendering without native modal fallback |

Example: Mixed Usage

<MenuModalPortalProvider>
  {/* Uses portal - rendered in top layer */}
  <MenuModal backgroundColor="#1a1a1a" options={normalOptions}>
    <Text>Normal Menu</Text>
  </MenuModal>

  {/* Opts out of portal - uses native modal instead */}
  <MenuModal
    backgroundColor="#1a1a1a"
    disablePortal
    options={fullscreenOptions}
  >
    <Text>Fullscreen Menu</Text>
  </MenuModal>

  <ScrollView>
    {/* Renders inline within ScrollView bounds */}
    <MenuModal
      backgroundColor="#1a1a1a"
      disablePortal
      disableNativeModal
      options={inlineOptions}
    >
      <Text>Constrained Menu</Text>
    </MenuModal>
  </ScrollView>
</MenuModalPortalProvider>

Summary: The package is flexible and works immediately without any setup. Add the portal provider for enhanced control, or use the disable props to customize rendering per menu instance.

Troubleshooting

Menu not appearing

  1. Ensure backgroundColor is set (it's required)
  2. Check that peer dependencies are installed and properly configured
  3. Verify react-native-reanimated is properly configured in babel.config.js:
    module.exports = {
      presets: ['module:metro-react-native-babel-preset'],
      plugins: ['react-native-reanimated/plugin'], // Must be last
    };
  4. Check console for any errors
  5. If using custom disablePortal and disableNativeModal props, ensure the parent container has appropriate dimensions

Portal-specific issues

Note: The portal provider is optional - the menu works fine without it using React Native's Modal component.

If you choose to use the portal provider:

  • Place MenuModalPortalProvider as high as possible in your component tree
  • If using React Navigation, place provider outside NavigationContainer
  • If using Expo Router, place it in app/_layout.tsx

"Cannot read property 'add' of undefined" error:

  • This only occurs if you use portal hooks (useMenuModalPortal, useMenuModalPortalComponent) without the provider
  • The MenuModal component itself does NOT require the provider and will work without this error

Menu position is off

  • This is handled by @shaquillehinds/react-native-spot-modal
  • Ensure the parent container doesn't have unusual transforms
  • Check if any absolute positioning is interfering

TypeScript errors

# Make sure types are properly installed
npm install --save-dev @types/react @types/react-native

Options not triggering

  • Verify onOptionPress is defined on the option
  • Check if disableDismissOnPress is interfering
  • Ensure the option isn't disabled via custom props

Related Packages

Contributing

Contributions are welcome! Please read the contributing guide and code of conduct.

Development Setup

# Clone the repository
git clone https://github.com/shaquillehinds/react-native-menu-modal.git

# Install dependencies
cd react-native-menu-modal
npm install

# Build the package
npm run prepare

# Run tests
npm test

# Lint code
npm run lint

License

MIT © Shaquille Hinds

Support


Made with ❤️ by Shaquille Hinds