Building a Native Menu System for iOS and Android with NativeScript

Deliver a production-quality native menu experience on both iOS and Android while keeping one shared TypeScript API.

Nathan Walker
Posted on

Great menus are one of those details users do not consciously praise, but they absolutely feel.

Tap a small floating action button and the right choices appear in the right place, with the right motion, the right visual weight, and the right platform personality. If that interaction is off by even a little, it feels cheap.

That was the goal behind @nstudio/nativescript-menu: deliver a production-quality native menu experience on both iOS and Android while keeping one shared TypeScript API. Credit to my colleague, Igor Randjelovic, for the inspiration!

Honoring the native feel of the platform

From the app side, the mental model should be simple:

  • define menu options in TypeScript
  • bind them to a view
  • handle selected events

From the platform side, the implementation should be unapologetically native:

  • iOS should use UIMenu and UIAction for the most native rendering and behavior
  • Android should use a native anchored presentation with polished motion and layering

That is exactly how this package is built.

Shared TypeScript Model

Everything starts with a shared cross-platform menu model. You describe options once, including nested menus, subtitles, destructive states, single-selection groups, and icons.

typescript
export interface MenuAction<T = any> {
  id?: number
  name?: string
  icon?: SystemIcon | ImageIcon | FontIcon
  iconColor?: Color | string
  destructive?: boolean
  disabled?: boolean
  hidden?: boolean
  keepsMenuOpen?: boolean
  subtitle?: string
  children?: Array<MenuAction>
  childrenStyle?: 'inline' | 'dropdown' | 'palette'
  singleSelection?: boolean
  state?: 'on' | 'off' | 'mixed'
  action?: (action: MenuAction<T>) => void
}

That model is intentionally expressive enough to map well to native APIs on both platforms.

iOS: Direct UIMenu and UIAction

On iOS, the implementation handles the following:

  • nodes with children become UIMenu
  • leaf nodes become UIAction
  • state maps to UIMenuElementState
  • destructive and disabled map to native attributes
  • subtitles supported on iOS 15+

The plugin also registers menu and contextMenu properties on View, so you can attach menus broadly, not only to buttons.

For non-button views, a transparent UIButton overlay is used when needed, which lets the menu present as a primary tap action while keeping your existing layout and styling intact.

The result is exactly what you want on iOS: native menu rendering, native behavior, and no abstraction lag.

Android: Native Anchored Glass Controller

Android is where we added some custom presentation work.

Rather than trying to mimic iOS through a generic popup, the implementation uses a native controller, GlassAnchoredMenuController.java, that is purpose-built for this interaction pattern.

Key details include:

  • anchored positioning from the trigger view
  • automatic above/below flip when space is constrained
  • edge collision handling and clamping
  • root arrow pointer alignment
  • nested submenu choreography (direction-aware)
  • palette-style icon row support
  • spring-style entrance animation
  • animated close on selection
  • drag-to-dismiss support on the root menu
  • haptic feedback on interactions

During iteration, we also hardened stability for real app conditions:

  • popup token handling is tied to a stable host window anchor
  • submenu stack management avoids stale popup layers
  • dismissal sequencing is coordinated to prevent flicker and race behavior

Icon Fidelity Across Platforms

Cross-platform icon handling is always tricky. iOS has SF Symbols. Android has app vector resources and different defaults.

To map this cleanly, the Android implementation serializes rich icon metadata to native:

  • iconType (symbol, src, font)
  • icon source
  • font family and weight
  • explicit tint color

The native Android controller then resolves the best rendering path per item:

  • app drawable or mipmap resource when available
  • mapped native icon fallback for common symbols
  • font glyph rendering with weight and tint

This keeps the TypeScript API unified while preserving platform-quality output.

View Layer: Simple Binding, Native Outcome

With view markup, the usage stays concise:

tsx
<MenuImage src={imageIcon} options={addOptions} selected={selectOption} />

The UI can be as minimal as a centered trigger, or part of a richer card-based layout. The menu behavior and native presentation remain consistent.

Why This Matters

There are two ways to build cross-platform interactions:

  1. force a single generic UI layer onto every platform
  2. keep a shared app-facing API but let each platform be itself

This plugin chooses option two.

That choice gives you:

  • less platform compromise
  • fewer visual seams
  • better interaction quality where users notice it most

Docs

For installation, usage, API, events, please see the documentation:

Final Thought

Menus are deceptively small UI elements, but they carry a lot of product quality signal.

With @nstudio/nativescript-menu, you get a single TypeScript configuration model and two native implementations tuned for each platform’s strengths. That is the sweet spot for NativeScript: shared developer ergonomics, native user experience.


More from our Blog