Install and first shape
npx expo install react-native-svgUse expo install, not npm install. This package has a native side, and a version that does not match your SDK produces a red screen about a missing native module rather than anything that names the real problem. It ships inside Expo Go, so no development build is required.
There is no <img> here. An SVG is described as components, and each element becomes a real native view:
import Svg, { Path } from 'react-native-svg';
export function CheckIcon({ size = 24, color = '#fb923c' }) {
return (
<Svg width={size} height={size} viewBox="0 0 24 24" fill="none">
<Path
d="M20 6L9 17l-5-5"
stroke={color}
strokeWidth={2}
strokeLinecap="round"
strokeLinejoin="round"
/>
</Svg>
);
}viewBox is the coordinate system the path was drawn in; width and height are the size on screen. Keep both. Omit the viewBox and the path is drawn at its original scale in a box that may be much smaller. Omit width and height and the whole thing lays out at zero and disappears.
| Element | What it does | In practice |
|---|---|---|
Svg | Root — needs width, height, and viewBox | Everything else nests inside |
Path | Any shape, from the d attribute | 95% of exported icons |
Circle / Rect / Line | Simple primitives | Hand-drawn shapes, rings, bars |
G | Group with shared transform or fill | Cheaper than repeating props |
Defs + LinearGradient | Reusable paint definitions | Referenced by url(#id) |
ClipPath / Mask | Cut shapes out of other shapes | Avatars, progress arcs |
Importing .svg files directly
Hand-transcribing every icon into JSX gets old at about icon four. The transformer compiles .svg files into components at bundle time:
npm install --save-dev react-native-svg-transformer// metro.config.js
const { getDefaultConfig } = require('expo/metro-config');
const config = getDefaultConfig(__dirname);
config.transformer.babelTransformerPath = require.resolve('react-native-svg-transformer');
// svg must move OUT of assetExts and INTO sourceExts — it is compiled, not copied
config.resolver.assetExts = config.resolver.assetExts.filter((ext) => ext !== 'svg');
config.resolver.sourceExts = [...config.resolver.sourceExts, 'svg'];
module.exports = config;TypeScript needs to be told what an .svg import is, or it rejects the line before Metro ever sees it:
// declarations.d.ts (include it in tsconfig)
declare module '*.svg' {
import type React from 'react';
import type { SvgProps } from 'react-native-svg';
const content: React.FC<SvgProps>;
export default content;
}import Logo from '../assets/logo.svg';
<Logo width={120} height={32} color="#fb923c" />Then run npx expo start --clear. Metro caches how it resolved every extension, so without clearing it will keep serving .svg through the old asset pipeline and you will conclude the config did not work. This is the single most common reason people give up on the transformer.
Recolouring icons with one prop
An icon with fill="#000000" baked into every path is a new file for every colour. Replace the hardcoded value with currentColor, which react-native-svg resolves from the color prop on the root:
<!-- assets/heart.svg -->
<svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<path fill="currentColor" d="M12 21s-8-4.9-8-10.3A4.7 4.7 0 0 1 12 7a4.7 4.7 0 0 1 8 3.7C20 16.1 12 21 12 21z"/>
</svg>import Heart from '../assets/heart.svg';
// one file, every state
<Heart width={24} height={24} color={liked ? '#fb923c' : 'rgba(255,255,255,0.35)'} />The same substitution is what makes an icon set theme-aware for free: pass the colour from your theme context and every icon follows light and dark mode without a second asset. For gradients, define the paint once in Defs and reference it by id:
import Svg, { Defs, LinearGradient, Rect, Stop } from 'react-native-svg';
<Svg width="100%" height={120} viewBox="0 0 100 40" preserveAspectRatio="none">
<Defs>
<LinearGradient id="warm" x1="0" y1="0" x2="1" y2="0">
<Stop offset="0" stopColor="#fb923c" stopOpacity="1" />
<Stop offset="1" stopColor="#f43f5e" stopOpacity="1" />
</LinearGradient>
</Defs>
<Rect x="0" y="0" width="100" height="40" fill="url(#warm)" rx="4" />
</Svg>Four reasons an SVG renders blank
- The file styles itself with CSS. Figma and Illustrator happily export
<style>.cls-1{fill:#000}</style>orstyle="fill:#000". react-native-svg implements SVG presentation attributes, not a CSS engine, so those rules are dropped and the shapes render with default paint. Re-export with presentation attributes, or run the file through SVGOMG with prefer presentation attributes turned on. - No dimensions. A root
Svgwith no width and height lays out at zero in flexbox. It is not clipped or hidden — it genuinely occupies no space. Always set both, or aflex: 1parent with explicit bounds. - Unsupported elements.
<filter>,<foreignObject>, and animated<animate>tags are either partially supported or ignored. Drop shadows in particular need to be a React Native shadow on a wrapping View, not an SVG filter. - Hundreds of paths. Every element is a real view, so a detailed illustration can be a thousand of them. That is a genuine scroll cost in a list. If an SVG has more than a few dozen paths and never needs to recolour or scale past 3x, export it as a PNG — the performance guide covers the wider view-count problem.
What most apps actually need
In practice most apps touch react-native-svg in three places: an icon set, one or two illustrations on empty states and onboarding, and whatever their charting library renders. If that is you, install the package, add the transformer, sanitise your exports, and you are done — the deeper API surface is for when you need custom drawing.
If you would rather start from a working screen than from an install command, describe it — “an empty state with an illustration, a headline, and a button” — and ShipNative generates it as real React Native, running on your phone, with the SVG plumbing already wired up. Export the project whenever you want to take it further by hand.