@westwood-dev/expo-dynamic-app-icon
v0.1.0
Published
Expo module for dynamic app icon switching on iOS, with graceful fallback on Android and favicon switching on web
Maintainers
Readme
@westwood-dev/expo-dynamic-app-icon
Expo module for switching app icons at runtime. Works on iOS (alternate icons via UIApplication), web (favicon swapping), and handles Android gracefully with a clear "not supported" error.
Platform support
| Platform | isSupported() | setIcon() | getCurrentIconName() |
|----------|----------------|-------------|----------------------|
| iOS | ✅ true | ✅ changes alternate icon | ✅ returns current icon name |
| Android | ❌ false | ❌ rejects with error | returns null |
| Web | ✅ true | ✅ swaps <link rel="icon"> | ✅ returns current icon name |
Installation
npx expo install @westwood-dev/expo-dynamic-app-iconiOS
Add the config plugin to your app.json/app.config.js. Provide an icons array — each entry needs a name (used at runtime to switch to that icon) and an image path (relative to your project root):
{
"expo": {
"plugins": [
[
"@westwood-dev/expo-dynamic-app-icon",
{
"icons": [
{ "name": "dark", "image": "./assets/icons/dark.png" },
{ "name": "light", "image": "./assets/icons/light.png" },
{ "name": "minimal", "image": "./assets/icons/minimal.png" }
]
}
]
]
}
}Then rebuild your native project:
npx expo prebuild --clean
npx expo run:iosThe config plugin copies each icon PNG into your Xcode project and registers it in
Info.plistunderCFBundleAlternateIcons. This is required by Apple — icons must be bundled at build time.
Android
No setup required. isSupported() returns false and setIcon() rejects with an error. Android does not support alternate home screen icons via a public API.
Web
No build-time setup required. On web, setIcon(iconName) dynamically updates <link rel="icon"> to point to /favicon-{iconName}.png. You are responsible for serving these files from your web public directory.
For example, calling setIcon('dark') sets the favicon href to /favicon-dark.png. Calling setIcon(null) resets to the original favicon that was present when the module first loaded.
API
import {
setIcon,
getCurrentIconName,
isSupported,
} from '@westwood-dev/expo-dynamic-app-icon';isSupported(): boolean
Returns true if the current platform supports dynamic icon switching.
- iOS:
truewhenUIApplication.shared.supportsAlternateIconsis true - Web:
truewhendocumentis available - Android: always
false
Always check this before calling setIcon.
setIcon(iconName: string | null): Promise<void>
Switches to the named icon. Pass null to reset to the primary (default) icon.
Rejects if:
- The platform is not supported (
isSupported()returnsfalse) - The icon name was not registered in the config plugin (iOS)
- iOS system rejects the change for any other reason
try {
await setIcon('dark');
} catch (e) {
console.warn('Icon switch failed:', e);
}getCurrentIconName(): string | null
Returns the name of the currently active alternate icon, or null if the primary icon is active. On Android always returns null.
Usage example
import { setIcon, getCurrentIconName, isSupported } from '@westwood-dev/expo-dynamic-app-icon';
import { useState, useEffect } from 'react';
import { Button, Platform, Text, View } from 'react-native';
export default function IconSwitcher() {
const [current, setCurrent] = useState<string | null>(null);
const supported = isSupported();
useEffect(() => {
setCurrent(getCurrentIconName());
}, []);
const switchTo = async (name: string | null) => {
try {
await setIcon(name);
setCurrent(name);
} catch (e) {
console.warn(e);
}
};
if (!supported) {
return <Text>Dynamic icons not supported on {Platform.OS}</Text>;
}
return (
<View>
<Text>Current icon: {current ?? 'default'}</Text>
<Button title="Dark icon" onPress={() => switchTo('dark')} />
<Button title="Light icon" onPress={() => switchTo('light')} />
<Button title="Reset" onPress={() => switchTo(null)} />
</View>
);
}iOS notes
- Apple requires a system dialog when the icon changes — this cannot be suppressed.
- Icons must be plain PNG files (no transparency layer as the primary icon shape).
- Recommended sizes: 1024×1024 px. iOS scales them automatically.
isSupported()returnsfalsein the simulator for some Xcode configurations even when the app is correctly set up. Test on a real device.
Web notes
setIconconvention: icon name"dark"→ favicon href/favicon-dark.png.- The original favicon is snapshotted on the first
setIconcall, sosetIcon(null)always resets to what was in the<link rel="icon">tag at load time. - If no
<link rel="icon">tag exists, it is created and appended to<head>.
Android notes
- Calling
setIcon()on Android throws:"Dynamic icon switching is not supported on this platform". - Guard with
isSupported()to avoid the error, or wrap in a try/catch.
License
MIT
