SVG Icons in React Native & Expo: The Complete 2026 Guide
How to use SVG icons in React Native and Expo — react-native-svg, @expo/vector-icons, lucide-react-native, and building accessible icon components with proper tap targets.
SVG icons in React Native need a different approach than the web. The browser SVG renderer isn’t available — React Native uses its own rendering pipeline, which means react-native-svg is required as a native bridge. Once that’s in place, the icon ecosystem becomes rich: Expo Vector Icons, lucide-react-native, and SVGR-generated components all work cleanly.
This guide covers every layer: native SVG support, the three icon library options, tap target sizing, and dark mode with React Native’s useColorScheme.
The React Native SVG stack
Your icon component
↓
lucide-react-native / @expo/vector-icons
↓
react-native-svg (JS bridge)
↓
Native SVG renderer (iOS CoreGraphics / Android Canvas)
react-native-svg must be installed and linked before any SVG icon library works. It’s the native module that renders SVG elements on both platforms.
Step 1: Install react-native-svg
Expo managed workflow (recommended)
npx expo install react-native-svg
Expo handles native linking automatically — no pod install needed.
Bare React Native
npm install react-native-svg
cd ios && pod install
Verify installation:
import Svg, { Circle } from 'react-native-svg';
// Should render a circle — if it does, react-native-svg works
<Svg width="50" height="50" viewBox="0 0 100 100">
<Circle cx="50" cy="50" r="40" fill="blue" />
</Svg>
Every SVG icon library for React Native uses react-native-svg internally. If icons render as blank or crash, it’s almost always because react-native-svg isn’t installed or linked correctly.
Option 1: @expo/vector-icons
If you’re using Expo, @expo/vector-icons ships pre-installed. It wraps popular icon fonts (Ionicons, MaterialIcons, FontAwesome, Feather, and more) as React Native components.
import { Ionicons } from '@expo/vector-icons';
import { Feather } from '@expo/vector-icons';
<Ionicons name="search" size={24} color="#374151" />
<Feather name="settings" size={24} color="#374151" />
Despite the name, @expo/vector-icons renders icon fonts, not SVGs. Icon fonts scale well but don’t support arbitrary color fills or complex paths. For true SVG rendering, use lucide-react-native or SVGR components.
Option 2: lucide-react-native (recommended)
lucide-react-native is the official Lucide port for React Native — built on top of react-native-svg, pure SVG rendering, same 1,500+ icons as the web version.
npm install lucide-react-native react-native-svg
import { Search, Settings, Bell, Home } from 'lucide-react-native';
function AppHeader() {
return (
<View style={{ flexDirection: 'row', gap: 16 }}>
<Search size={24} color="#374151" />
<Settings size={24} color="#374151" />
<Bell size={24} color="#374151" strokeWidth={1.5} />
</View>
);
}
Props
| Prop | Type | Default | Notes |
|---|---|---|---|
size | number | 24 | Width and height |
color | string | #000 | Stroke color (no currentColor in RN) |
strokeWidth | number | 2 | Stroke weight |
style | ViewStyle | — | RN style prop |
absoluteStrokeWidth | boolean | false | Keep stroke width fixed at all sizes |
React Native doesn’t have CSS inheritance, so currentColor isn’t supported. Always pass color explicitly. For dark mode, read the system color scheme and derive colors programmatically (see below).
Option 3: SVGR for custom icons
For icons not in any library, generate React Native components from SVG files using SVGR:
npm install --save-dev @svgr/cli
npx svgr --native --out-dir src/icons --typescript assets/icons/
SVGR converts each .svg to a .tsx that uses react-native-svg primitives:
// Generated: src/icons/CustomLogo.tsx
import Svg, { Path } from 'react-native-svg';
import type { SvgProps } from 'react-native-svg';
export function CustomLogo({ width = 24, height = 24, color = '#000', ...props }: SvgProps) {
return (
<Svg width={width} height={height} viewBox="0 0 24 24" fill="none" {...props}>
<Path d="M12 2L15.09 8.26L22 9.27L17 14.14..." stroke={color} strokeWidth="2" />
</Svg>
);
}
Tap target sizing
The most common mobile icon mistake: icons too small to tap reliably. iOS HIG and Android Material Design both require 44×44dp minimum tap targets.
An 18px icon in a 24px container gives only a 24dp tap target — frustrating on mobile, and a WCAG failure.
Fix with a wrapper:
import { TouchableOpacity, View } from 'react-native';
function IconButton({ icon: Icon, onPress, label, size = 22, color = '#374151' }) {
return (
<TouchableOpacity
onPress={onPress}
accessibilityLabel={label}
accessibilityRole="button"
style={{
minWidth: 44,
minHeight: 44,
alignItems: 'center',
justifyContent: 'center',
}}
>
<Icon size={size} color={color} />
</TouchableOpacity>
);
}
Dark mode with useColorScheme
React Native exposes the system color scheme through useColorScheme:
import { useColorScheme } from 'react-native';
import { Search, Settings } from 'lucide-react-native';
function NavBar() {
const colorScheme = useColorScheme();
const iconColor = colorScheme === 'dark' ? '#d1d5db' : '#374151';
return (
<View style={{ flexDirection: 'row', gap: 16 }}>
<Search size={22} color={iconColor} />
<Settings size={22} color={iconColor} />
</View>
);
}
For a design system, define a color hook:
// hooks/useIconColors.ts
import { useColorScheme } from 'react-native';
export function useIconColors() {
const scheme = useColorScheme();
return {
default: scheme === 'dark' ? '#d1d5db' : '#374151',
muted: scheme === 'dark' ? '#6b7280' : '#9ca3af',
accent: scheme === 'dark' ? '#60a5fa' : '#2563eb',
danger: scheme === 'dark' ? '#f87171' : '#dc2626',
};
}
function Icon() {
const colors = useIconColors();
return <Search size={22} color={colors.default} />;
}
Bottom tab bar icons
React Navigation’s bottom tab bar uses icons through the tabBarIcon option:
import { createBottomTabNavigator } from '@react-navigation/bottom-tabs';
import { Home, Search, Bell, User } from 'lucide-react-native';
const Tab = createBottomTabNavigator();
function AppTabs() {
const colors = useIconColors();
return (
<Tab.Navigator
screenOptions={({ route }) => ({
tabBarIcon: ({ focused, size }) => {
const icons = {
Home: Home,
Search: Search,
Notifications: Bell,
Profile: User,
};
const Icon = icons[route.name];
return (
<Icon
size={size}
color={focused ? colors.accent : colors.muted}
strokeWidth={focused ? 2.5 : 1.5}
/>
);
},
})}
>
<Tab.Screen name="Home" component={HomeScreen} />
<Tab.Screen name="Search" component={SearchScreen} />
<Tab.Screen name="Notifications" component={NotificationsScreen} />
<Tab.Screen name="Profile" component={ProfileScreen} />
</Tab.Navigator>
);
}
The strokeWidth change (1.5 → 2.5) on active is a subtle but effective active state signal that doesn’t rely on color alone.
Accessibility in React Native
// Decorative icon in labeled button
<TouchableOpacity accessibilityLabel="Export CSV" accessibilityRole="button">
<Download size={20} color={colors.default} />
<Text>Export CSV</Text>
</TouchableOpacity>
// Icon-only button
<TouchableOpacity
accessibilityLabel="Delete item"
accessibilityRole="button"
style={{ minWidth: 44, minHeight: 44, alignItems: 'center', justifyContent: 'center' }}
>
<Trash2 size={20} color={colors.danger} />
</TouchableOpacity>
accessibilityLabel is the React Native equivalent of aria-label. Screen readers (VoiceOver on iOS, TalkBack on Android) use it to announce the button’s purpose.