├── .yarnrc.yml ├── assets └── images │ ├── icon.png │ ├── moon.png │ ├── sun.png │ ├── threeDots.png │ ├── splash-icon.png │ └── adaptive-icon.png ├── tsconfig.json ├── app ├── index.tsx ├── _layout.tsx └── myNotes.tsx ├── .gitignore ├── constants ├── MenuActions.ts └── SampleNotes.ts ├── components └── NotesList.tsx ├── app.json ├── package.json ├── plugins └── android │ └── withRoundedPopupMenu.js └── README.md /.yarnrc.yml: -------------------------------------------------------------------------------- 1 | nodeLinker: node-modules 2 | -------------------------------------------------------------------------------- /assets/images/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/arunabhverma/expo-android-native-menu/HEAD/assets/images/icon.png -------------------------------------------------------------------------------- /assets/images/moon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/arunabhverma/expo-android-native-menu/HEAD/assets/images/moon.png -------------------------------------------------------------------------------- /assets/images/sun.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/arunabhverma/expo-android-native-menu/HEAD/assets/images/sun.png -------------------------------------------------------------------------------- /assets/images/threeDots.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/arunabhverma/expo-android-native-menu/HEAD/assets/images/threeDots.png -------------------------------------------------------------------------------- /assets/images/splash-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/arunabhverma/expo-android-native-menu/HEAD/assets/images/splash-icon.png -------------------------------------------------------------------------------- /assets/images/adaptive-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/arunabhverma/expo-android-native-menu/HEAD/assets/images/adaptive-icon.png -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "expo/tsconfig.base", 3 | "compilerOptions": { 4 | "strict": true, 5 | "paths": { 6 | "@/*": [ 7 | "./*" 8 | ] 9 | } 10 | }, 11 | "include": [ 12 | "**/*.ts", 13 | "**/*.tsx", 14 | ".expo/types/**/*.ts", 15 | "expo-env.d.ts" 16 | ] 17 | } 18 | -------------------------------------------------------------------------------- /app/index.tsx: -------------------------------------------------------------------------------- 1 | import { StyleSheet, Text, View } from "react-native"; 2 | import React from "react"; 3 | import { useTheme } from "@react-navigation/native"; 4 | import { Link } from "expo-router"; 5 | 6 | export default function Main() { 7 | const theme = useTheme(); 8 | return ( 9 | 10 | 11 | My Notes 12 | 13 | 14 | ); 15 | } 16 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Learn more https://docs.github.com/en/get-started/getting-started-with-git/ignoring-files 2 | 3 | # dependencies 4 | node_modules/ 5 | 6 | # Expo 7 | .expo/ 8 | dist/ 9 | web-build/ 10 | expo-env.d.ts 11 | 12 | # Native 13 | *.orig.* 14 | *.jks 15 | *.p8 16 | *.p12 17 | *.key 18 | *.mobileprovision 19 | 20 | # Native folders 21 | /android/ 22 | /ios/ 23 | 24 | 25 | # Metro 26 | .metro-health-check* 27 | 28 | # debug 29 | npm-debug.* 30 | yarn-debug.* 31 | yarn-error.* 32 | 33 | # macOS 34 | .DS_Store 35 | *.pem 36 | 37 | # local env files 38 | .env*.local 39 | 40 | # typescript 41 | *.tsbuildinfo 42 | 43 | app-example 44 | -------------------------------------------------------------------------------- /constants/MenuActions.ts: -------------------------------------------------------------------------------- 1 | export const MENU_ACTIONS = [ 2 | { 3 | id: "edit", 4 | title: "Edit", 5 | }, 6 | { 7 | id: "share", 8 | title: "Share", 9 | 10 | }, 11 | { id: "addLabel", 12 | title: "Add Label", 13 | 14 | }, 15 | { 16 | id: "changeColor", 17 | title: "Change Color", 18 | }, 19 | { 20 | id: "export", 21 | title: "Export", 22 | subactions: [{ 23 | id: "exportAsPDF", 24 | title: "Export as PDF", 25 | }, 26 | { 27 | id: "exportAsImage", 28 | title: "Export as Image", 29 | }, 30 | { 31 | id: "copyToClipboard", 32 | title: "Copy to Clipboard", 33 | }, 34 | ], 35 | }, 36 | { 37 | id: "delete", 38 | title: "Delete", 39 | attributes: { 40 | destructive: true, 41 | } 42 | }, 43 | ]; 44 | -------------------------------------------------------------------------------- /app/_layout.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | DarkTheme, 3 | DefaultTheme, 4 | ThemeProvider, 5 | } from "@react-navigation/native"; 6 | import { Stack } from "expo-router"; 7 | import { StatusBar } from "expo-status-bar"; 8 | import { useColorScheme } from "react-native"; 9 | import "react-native-reanimated"; 10 | 11 | declare module "@react-navigation/native" { 12 | export type ExtendedTheme = { 13 | dark: boolean; 14 | colors: { 15 | primary: string; 16 | background: string; 17 | card: string; 18 | text: string; 19 | border: string; 20 | notification: string; 21 | }; 22 | }; 23 | export function useTheme(): ExtendedTheme; 24 | } 25 | 26 | export default function RootLayout() { 27 | const colorScheme = useColorScheme(); 28 | 29 | let dark = { 30 | ...DarkTheme, 31 | colors: { 32 | ...DarkTheme.colors, 33 | background: "#181818", 34 | card: "#181818", 35 | text: "#FFFFFF", 36 | }, 37 | }; 38 | let light = { 39 | ...DefaultTheme, 40 | colors: { 41 | ...DefaultTheme.colors, 42 | background: "#FDFBF7", 43 | card: "#FDFBF7", 44 | text: "#000000", 45 | }, 46 | }; 47 | const theme = colorScheme === "dark" ? dark : light; 48 | 49 | return ( 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | ); 58 | } 59 | -------------------------------------------------------------------------------- /components/NotesList.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { View, Text, StyleSheet, ScrollView, Pressable } from "react-native"; 3 | import { useTheme } from "@react-navigation/native"; 4 | import { SAMPLE_NOTES } from "@/constants/SampleNotes"; 5 | 6 | export const NotesList = () => { 7 | const theme = useTheme(); 8 | 9 | return ( 10 | 14 | {SAMPLE_NOTES.map((note) => ( 15 | 16 | 17 | 18 | {note.title} 19 | 20 | 21 | {note.date} 22 | 23 | 24 | 25 | {note.content} 26 | 27 | 28 | ))} 29 | 30 | ); 31 | }; 32 | 33 | const styles = StyleSheet.create({ 34 | container: { 35 | flex: 1, 36 | }, 37 | noteCard: { 38 | paddingHorizontal: 20, 39 | paddingVertical: 16, 40 | }, 41 | noteHeader: { 42 | flexDirection: "row", 43 | justifyContent: "space-between", 44 | alignItems: "center", 45 | marginBottom: 8, 46 | }, 47 | noteTitle: { 48 | fontSize: 18, 49 | fontWeight: "600", 50 | }, 51 | noteDate: { 52 | fontSize: 12, 53 | opacity: 0.7, 54 | }, 55 | noteContent: { 56 | fontSize: 14, 57 | lineHeight: 20, 58 | }, 59 | }); 60 | -------------------------------------------------------------------------------- /app.json: -------------------------------------------------------------------------------- 1 | { 2 | "expo": { 3 | "name": "expo-android-native-menu", 4 | "slug": "expo-android-native-menu", 5 | "version": "1.0.0", 6 | "orientation": "portrait", 7 | "icon": "./assets/images/icon.png", 8 | "scheme": "myapp", 9 | "userInterfaceStyle": "automatic", 10 | "newArchEnabled": true, 11 | "ios": { 12 | "supportsTablet": true 13 | }, 14 | "android": { 15 | "adaptiveIcon": { 16 | "foregroundImage": "./assets/images/adaptive-icon.png", 17 | "backgroundColor": "#ffffff" 18 | }, 19 | "package": "com.arunabhdecaf.expoandroidnativemenu" 20 | }, 21 | "web": { 22 | "bundler": "metro", 23 | "output": "static", 24 | "favicon": "./assets/images/favicon.png" 25 | }, 26 | "plugins": [ 27 | "expo-router", 28 | [ 29 | "react-native-edge-to-edge", 30 | { 31 | "android": { 32 | "parentTheme": "Material3", 33 | "enforceNavigationBarContrast": false 34 | } 35 | } 36 | ], 37 | [ 38 | "./plugins/android/withRoundedPopupMenu", 39 | { 40 | "lightBackgroundColor": "#FFFFFF", 41 | "darkBackgroundColor": "#212121", 42 | "radius": 14, 43 | "paddingVertical": 8 44 | } 45 | ], 46 | [ 47 | "expo-splash-screen", 48 | { 49 | "image": "./assets/images/splash-icon.png", 50 | "imageWidth": 200, 51 | "resizeMode": "contain", 52 | "backgroundColor": "#ffffff" 53 | } 54 | ], 55 | "expo-asset" 56 | ], 57 | "experiments": { 58 | "typedRoutes": true 59 | } 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /constants/SampleNotes.ts: -------------------------------------------------------------------------------- 1 | export interface Note { 2 | id: number; 3 | title: string; 4 | content: string; 5 | date: string; 6 | } 7 | 8 | export const SAMPLE_NOTES: Note[] = [ 9 | { 10 | id: 1, 11 | title: "Meeting Notes", 12 | content: "Discuss project timeline and deliverables for Q2", 13 | date: "2024-03-20", 14 | }, 15 | { 16 | id: 2, 17 | title: "Shopping List", 18 | content: "Milk, eggs, bread, fruits", 19 | date: "2024-03-19", 20 | }, 21 | { 22 | id: 3, 23 | title: "Book Recommendations", 24 | content: "Atomic Habits, Deep Work, The Psychology of Success", 25 | date: "2024-03-18", 26 | }, 27 | { 28 | id: 4, 29 | title: "Workout Plan", 30 | content: "30 min cardio, strength training, stretching", 31 | date: "2024-03-17", 32 | }, 33 | { 34 | id: 5, 35 | title: "Travel Plans", 36 | content: "Book flights for summer vacation, research hotels", 37 | date: "2024-03-16", 38 | }, 39 | { 40 | id: 6, 41 | title: "Movie Watchlist", 42 | content: "Inception, The Matrix, Interstellar", 43 | date: "2024-03-15", 44 | }, 45 | { 46 | id: 7, 47 | title: "Project Ideas", 48 | content: "Mobile app for task management, website redesign", 49 | date: "2024-03-14", 50 | }, 51 | { 52 | id: 8, 53 | title: "Health Goals", 54 | content: "Drink more water, get 8 hours of sleep, meditate daily", 55 | date: "2024-03-13", 56 | }, 57 | { 58 | id: 9, 59 | title: "Learning Goals", 60 | content: "Complete React Native course, learn TypeScript", 61 | date: "2024-03-12", 62 | }, 63 | { 64 | id: 10, 65 | title: "Home Improvement", 66 | content: "Paint living room, organize garage, fix leaky faucet", 67 | date: "2024-03-11", 68 | }, 69 | ]; 70 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "expo-android-native-menu", 3 | "main": "expo-router/entry", 4 | "version": "1.0.0", 5 | "scripts": { 6 | "start": "expo start", 7 | "reset-project": "node ./scripts/reset-project.js", 8 | "android": "expo run:android", 9 | "ios": "expo run:ios", 10 | "web": "expo start --web", 11 | "test": "jest --watchAll", 12 | "lint": "expo lint" 13 | }, 14 | "jest": { 15 | "preset": "jest-expo" 16 | }, 17 | "dependencies": { 18 | "@expo/vector-icons": "^14.0.2", 19 | "@react-native-menu/menu": "^1.2.3", 20 | "@react-navigation/bottom-tabs": "^7.2.0", 21 | "@react-navigation/native": "^7.0.14", 22 | "expo": "~52.0.46", 23 | "expo-blur": "~14.0.3", 24 | "expo-constants": "~17.0.8", 25 | "expo-font": "~13.0.4", 26 | "expo-haptics": "~14.0.1", 27 | "expo-linking": "~7.0.5", 28 | "expo-router": "~4.0.20", 29 | "expo-splash-screen": "~0.29.24", 30 | "expo-status-bar": "~2.0.1", 31 | "expo-symbols": "~0.2.2", 32 | "expo-system-ui": "~4.0.9", 33 | "expo-web-browser": "~14.0.2", 34 | "metro": "^0.82.1", 35 | "react": "18.3.1", 36 | "react-dom": "18.3.1", 37 | "react-native": "0.76.9", 38 | "react-native-edge-to-edge": "^1.6.0", 39 | "react-native-gesture-handler": "~2.20.2", 40 | "react-native-reanimated": "~3.16.1", 41 | "react-native-safe-area-context": "4.12.0", 42 | "react-native-screens": "~4.4.0", 43 | "react-native-web": "~0.19.13", 44 | "react-native-webview": "13.12.5" 45 | }, 46 | "devDependencies": { 47 | "@babel/core": "^7.25.2", 48 | "@types/jest": "^29.5.12", 49 | "@types/react": "~18.3.12", 50 | "@types/react-test-renderer": "^18.3.0", 51 | "jest": "^29.2.1", 52 | "jest-expo": "~52.0.6", 53 | "react-test-renderer": "18.3.1", 54 | "typescript": "^5.3.3" 55 | }, 56 | "private": true, 57 | "packageManager": "yarn@4.2.2+sha512.c44e283c54e02de9d1da8687025b030078c1b9648d2895a65aab8e64225bfb7becba87e1809fc0b4b6778bbd47a1e2ab6ac647de4c5e383a53a7c17db6c3ff4b" 58 | } 59 | -------------------------------------------------------------------------------- /app/myNotes.tsx: -------------------------------------------------------------------------------- 1 | import { Appearance, Image, Pressable, View, StyleSheet } from "react-native"; 2 | import React, { useLayoutEffect } from "react"; 3 | import { MenuView } from "@react-native-menu/menu"; 4 | import { useTheme } from "@react-navigation/native"; 5 | import { useNavigation } from "expo-router"; 6 | import { NotesList } from "@/components/NotesList"; 7 | import { MENU_ACTIONS } from "@/constants/menuActions"; 8 | 9 | const Moon = require("../assets/images/moon.png"); 10 | const Sun = require("../assets/images/sun.png"); 11 | 12 | const ThemeToggle = () => { 13 | const theme = useTheme(); 14 | const isDark = Appearance.getColorScheme() === "dark"; 15 | 16 | const handleThemeToggle = () => { 17 | Appearance.setColorScheme(isDark ? "light" : "dark"); 18 | }; 19 | 20 | return ( 21 | 22 | 27 | 28 | ); 29 | }; 30 | 31 | const HeaderMenu = () => { 32 | const theme = useTheme(); 33 | const MENU_ACTION_TRANSFORMED = MENU_ACTIONS.map((action) => ({ 34 | ...action, 35 | titleColor: theme.colors.text, 36 | })); 37 | 38 | return ( 39 | {}} 44 | > 45 | 50 | 51 | ); 52 | }; 53 | 54 | const HeaderActions = () => { 55 | return ( 56 | 57 | 58 | 59 | 60 | ); 61 | }; 62 | 63 | export default function MyNotes() { 64 | const navigation = useNavigation(); 65 | 66 | useLayoutEffect(() => { 67 | navigation.setOptions({ 68 | headerRight: () => , 69 | }); 70 | }, [navigation]); 71 | 72 | return ; 73 | } 74 | 75 | const styles = StyleSheet.create({ 76 | themeIcon: { 77 | width: 20, 78 | height: 20, 79 | }, 80 | 81 | menuIcon: { 82 | width: 24, 83 | height: 24, 84 | }, 85 | menuHitSlop: { 86 | top: 20, 87 | bottom: 20, 88 | left: 20, 89 | right: 20, 90 | }, 91 | 92 | headerActions: { 93 | flexDirection: "row", 94 | alignItems: "center", 95 | gap: 18, 96 | }, 97 | }); 98 | -------------------------------------------------------------------------------- /plugins/android/withRoundedPopupMenu.js: -------------------------------------------------------------------------------- 1 | const { withDangerousMod } = require("@expo/config-plugins"); 2 | const fs = require("node:fs"); 3 | const path = require("node:path"); 4 | 5 | /** 6 | * Config plugin to add border radius to Android popup menus 7 | * @param {import('@expo/config-types').ExpoConfig} config - Expo config 8 | * @param {Object} options - Plugin options 9 | * @param {number} [options.radius=14] - Border radius in dp 10 | * @param {string} [options.lightBackgroundColor='#FFFFFF'] - Background color for light theme 11 | * @param {string} [options.darkBackgroundColor='#000000'] - Background color for dark theme 12 | * @param {number} [options.paddingVertical=14] - Vertical padding in dp 13 | * @param {number} [options.paddingHorizontal=0] - Horizontal padding in dp 14 | * @returns {import('@expo/config-types').ExpoConfig} - Modified Expo config 15 | */ 16 | const withRoundedPopupMenu = (config, options = {}) => { 17 | const radius = options.radius || 14; 18 | const lightBackgroundColor = options.lightBackgroundColor || "#FFFFFF"; 19 | const darkBackgroundColor = options.darkBackgroundColor || "#000000"; 20 | const paddingVertical = options.paddingVertical || 14; 21 | const paddingHorizontal = options.paddingHorizontal || 0; 22 | 23 | let modifiedConfig = withDangerousMod(config, [ 24 | "android", 25 | async (config) => { 26 | const androidDir = path.join(config.modRequest.platformProjectRoot, "app", "src", "main"); 27 | const resDir = path.join(androidDir, "res"); 28 | const drawableDir = path.join(resDir, "drawable"); 29 | const drawableNightDir = path.join(resDir, "drawable-night"); 30 | 31 | if (!fs.existsSync(drawableDir)) { 32 | fs.mkdirSync(drawableDir, { recursive: true }); 33 | } 34 | 35 | if (!fs.existsSync(drawableNightDir)) { 36 | fs.mkdirSync(drawableNightDir, { recursive: true }); 37 | } 38 | 39 | const roundedPopupLightXml = ` 40 | 41 | 42 | 43 | 48 | `; 49 | 50 | const roundedPopupDarkXml = ` 51 | 52 | 53 | 54 | 59 | `; 60 | 61 | fs.writeFileSync(path.join(drawableDir, "rounded_popup.xml"), roundedPopupLightXml); 62 | fs.writeFileSync(path.join(drawableNightDir, "rounded_popup.xml"), roundedPopupDarkXml); 63 | 64 | return config; 65 | }, 66 | ]); 67 | 68 | modifiedConfig = withDangerousMod(modifiedConfig, [ 69 | "android", 70 | async (config) => { 71 | const androidDir = path.join(config.modRequest.platformProjectRoot, "app", "src", "main"); 72 | const resDir = path.join(androidDir, "res"); 73 | const valuesDir = path.join(resDir, "values"); 74 | 75 | if (!fs.existsSync(valuesDir)) { 76 | fs.mkdirSync(valuesDir, { recursive: true }); 77 | } 78 | 79 | const stylesPath = path.join(valuesDir, "styles.xml"); 80 | 81 | const stylesXml = ` 82 | 83 | 86 | 87 | 90 | `; 91 | 92 | fs.writeFileSync(stylesPath, stylesXml); 93 | 94 | return config; 95 | }, 96 | ]); 97 | 98 | return modifiedConfig; 99 | }; 100 | 101 | module.exports = withRoundedPopupMenu; 102 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Expo Android Native Menu Styling 2 | 3 | This repository provides a complete implementation for customizing the Android native menu in your Expo app. This implementation is highly inspired by [this solution](https://github.com/react-native-menu/menu/issues/58#issuecomment-806530467) for styling Android popup menus. 4 | 5 | ## Demo 6 | 7 | | Android | 8 | | ----------------------------------------------------------------------------------------------- | 9 | |