Skip to main content

Babel Plugin

Bundle icons at build time for 0ms first render - no network requests, no loading states.

info

The Babel plugin is optional. rn-iconify works out of the box with runtime fetching. Use this for production optimization.

Installationโ€‹

The Babel plugin is included with rn-iconify:

npm install rn-iconify

Configurationโ€‹

Add to your babel.config.js:

module.exports = {
presets: ['module:metro-react-native-babel-preset'],
plugins: [
['rn-iconify/babel', {
// Icons are auto-detected from your source code
// No manual configuration needed!
}],
],
};

And wrap your Metro config, which is how the bundle reaches the app:

// metro.config.js
const { getDefaultConfig } = require('expo/metro-config'); // or @react-native/metro-config
const { withRnIconify } = require('rn-iconify/metro');

module.exports = withRnIconify(getDefaultConfig(__dirname));

How It Worksโ€‹

The Babel plugin automatically:

  1. Scans your code - Finds all icon usages like <Mdi name="home" />
  2. Collects icon names - Tracks which icons are used in each file
  3. Fetches at build time - Downloads SVG data from Iconify API
  4. Generates cache - Creates a local cache file with all icon data
  5. Instant runtime - Icons render immediately without network requests

The plugin never changes the code it compiles. The library reads the bundle itself, from rn-iconify/bundled-icons, which withRnIconify points at .rn-iconify/icons.js. Without the Metro wrapper that module is an empty bundle, and icons come from the disk cache or the network.

Before 5.0.0

The plugin used to inject a loadOfflineBundle() call into the first file importing rn-iconify that it compiled. Which file that was depended on build order, so the same file could compile two ways, and Metro's transform cache could pair one version's code with the other's dependencies โ€” failing on device with loadOfflineBundle is not a function. The autoInject option is gone with it; add withRnIconify to your Metro config instead.

// Your code - plugin detects these automatically
<Mdi name="home" />
<Heroicons name="user" />
prefetchIcons(['mdi:settings', 'lucide:camera'])

Configuration Optionsโ€‹

module.exports = {
plugins: [
['rn-iconify/babel', {
// Include specific patterns (supports wildcards)
include: [
'mdi:*', // All Material Design Icons
'heroicons:*', // All Heroicons
],

// Exclude patterns
exclude: [
'mdi:test-*', // Skip test icons
],

// Output path for generated cache (withRnIconify's outputDir must match)
outputPath: '.rn-iconify',

// Enable verbose logging
verbose: true,

// Disable the plugin temporarily
disabled: false,
}],
],
};

Options Referenceโ€‹

OptionTypeDefaultDescription
includestring[][]Icon patterns to include (wildcards supported)
excludestring[][]Icon patterns to exclude
outputPathstring.rn-iconifyGenerated cache location; withRnIconify's outputDir must match
verbosebooleanfalseLog detected icons during build
disabledbooleanfalseDisable the plugin

Pattern Matchingโ€‹

Use glob patterns for flexible icon selection:

plugins: [
['rn-iconify/babel', {
include: [
'mdi:home*', // home, home-outline, home-variant, etc.
'mdi:arrow-*', // All arrow icons
'heroicons:*-solid', // All solid Heroicons
],
exclude: [
'mdi:*-off', // Exclude disabled variants
],
}],
],

Auto-Detectionโ€‹

By default, the plugin auto-detects icons from your code:

// All of these are automatically detected:

// JSX usage
<Mdi name="home" />
<Heroicons name="user" />
<Lucide name="camera" />

// prefetchIcons calls
prefetchIcons(['mdi:home', 'mdi:settings'])

Bundle Size Impactโ€‹

Bundled icons add to your JavaScript bundle:

Icons BundledApproximate Size
10 icons~5 KB
100 icons~50 KB
1,000 icons~500 KB
warning

An icon the plugin does not detect is not missing โ€” it is fetched from the Iconify API at runtime, in release builds too. That is a request on every install, a placeholder until it lands, and nothing at all offline. Run npx rn-iconify doctor to see which icons are in that state.

danger

include and exclude filter what was detected. They never add anything. Setting include: ['mdi:loading'] does not bundle that icon โ€” it discards every icon that does not match, leaving the rest to be fetched at runtime. To add names the plugin cannot see, use defineIcons.

Names Chosen at Runtimeโ€‹

Most icon names are found in your source even when they are not written on the icon component. A component that declares an icon prop names its set in the type, so anything handed to it is detected:

// Detected โ€” the prop type says which set 'person-outline' belongs to
function Row({ icon }: { icon: IonIconName }) {
return <Ion name={icon} />;
}

<Row icon="person-outline" />;

What cannot be detected is a name the source never states as belonging to a set:

// Nothing here says 'hanger' is an Mdi icon
const CATEGORY_ICON: Record<string, string> = { OUTFIT: 'hanger' };

// Nor here โ€” the name does not exist until it runs
const iconName = `mdi:${state}`;

Declare those with defineIcons:

import { defineIcons } from 'rn-iconify';
import type { MdiIconName } from 'rn-iconify';

const CATEGORY_ICON = defineIcons<MdiIconName>({
OUTFIT: 'hanger',
SPOTLIGHT: 'theater',
});

const STATES = defineIcons<MdiIconName>(['loading', 'success', 'error']);

The type argument does two things: it stops a typo reaching a build, and it tells the plugin which set the names belong to.

Typing the map is equally good, and needs no extra call:

const CATEGORY_ICON: Record<string, MdiIconName> = { OUTFIT: 'hanger' };
tip

npx rn-iconify doctor lists every icon still being fetched at runtime, so you can see whether any are left.

Expo Configurationโ€‹

For Expo projects, modify babel.config.js:

module.exports = function (api) {
api.cache(true);
return {
presets: ['babel-preset-expo'],
plugins: [
['rn-iconify/babel', {
verbose: __DEV__, // Only log in development
}],
],
};
};

Clearing Cacheโ€‹

After changing configuration, clear Metro cache:

# React Native CLI
npx react-native start --reset-cache

# Expo
npx expo start --clear

Build Outputโ€‹

With verbose: true, you'll see detected icons during build:

[rn-iconify] Build started. Project root: /path/to/project
[rn-iconify] Detected 47 unique icons across 23 files
[rn-iconify] Icon sets: mdi (28), heroicons (12), lucide (7)
[rn-iconify] Generating bundle...
[rn-iconify] Bundle written to .rn-iconify/icons.json (15.2 KB)

Generated Filesโ€‹

The plugin creates a cache directory:

.rn-iconify/
โ”œโ”€โ”€ icons.json # Icon data the next build reads back
โ”œโ”€โ”€ icons.js # The module Metro serves as rn-iconify/bundled-icons
โ””โ”€โ”€ usage.json # Icons fetched at runtime in development

The bundle is deterministic: icons in name order, no timestamp, and written only when its contents change. Commit it the way you commit a lockfile โ€” it changes exactly when the code starts using an icon it did not before, and a fresh checkout or a CI build then renders every icon offline without fetching anything. The plugin only adds icons; npx rn-iconify doctor --prune removes the ones nothing uses any more.

Ignore only the lock the plugin takes while scanning:

.rn-iconify/.scan-lock

CI/CD Integrationโ€‹

Ensure icons are bundled in CI:

# .github/workflows/build.yml
- name: Build App
run: |
npm install
npx react-native bundle \
--platform android \
--dev false \
--entry-file index.js \
--bundle-output ./android/app/src/main/assets/index.android.bundle

Troubleshootingโ€‹

Icons Not Bundlingโ€‹

  1. Check icon component names are correct (Mdi, not mdi)
  2. Clear Metro cache
  3. Enable verbose mode to see detected icons
['rn-iconify/babel', {
verbose: true,
}]

Network Errors During Buildโ€‹

The plugin needs network access to fetch icons. If behind a proxy:

# Set proxy environment variables
export HTTP_PROXY=http://proxy.example.com:8080
export HTTPS_PROXY=http://proxy.example.com:8080

Large Bundle Sizeโ€‹

  1. Review what icons are being bundled (use verbose: true)
  2. Use specific includes instead of wildcards
  3. Add excludes for unused icon variants

Performance Comparisonโ€‹

MethodFirst RenderBundle SizeNetwork
Runtime fetch~100-500msMinimalRequired
Prefetch~0ms (after prefetch)MinimalRequired
Babel plugin0msIncreasesNone
  1. Development: Let plugin auto-detect icons
  2. Production: Review bundled icons, optimize with includes/excludes
  3. Critical path: Ensure navigation and common UI icons are bundled
// babel.config.js
const isProduction = process.env.NODE_ENV === 'production';

module.exports = {
plugins: [
['rn-iconify/babel', {
verbose: !isProduction,
// In production, explicitly include only needed icons
include: isProduction ? [
'mdi:home',
'mdi:settings',
'mdi:account',
'mdi:arrow-left',
] : [],
}],
],
};

Next Stepsโ€‹