個人開発で運用している壁紙アプリのタブアイコンを、線の細いオリジナルのSVGに差し替えたときのことです。Expo Go で開くと綺麗に表示されていたので安心して開発ビルドを作ったところ、同じ画面でアイコンだけがぽっかりと消えていました。エラーは出ません。ただ、そこにあるはずの図形が透明になっている。原因にたどり着くまで小一時間かかりましたので、同じところで立ち止まる方のために整理して共有します。
症状
SVGアイコンが絡むと、次のいずれかの形でつまずきます。
- Expo Go では表示されるのに、開発ビルドや本番ビルドでは何も描かれない
Unable to resolve module ./icon.svgという赤い画面が出る- 表示はされるが、ダークモードにしても色が黒いまま変わらない
- iOS では出るのに Android だけ表示されない(またはその逆)
厄介なのは、多くの場合エラーが出ずに「透明な図形」として静かに失敗する点です。レイアウト上の場所は確保されているのに中身が見えないため、CSSの問題だと勘違いして遠回りしがちでした。
再現条件
Rork が生成したコード、あるいは自分で追加したコードで、SVGファイルをこう読み込んでいるケースです。
import TabIcon from "../assets/icons/home.svg";
export function HomeTab() {
return <TabIcon width={24} height={24} />;
}一見なんの問題もありません。しかし React Native / Expo の Metro バンドラーは、初期状態では .svg を「画像アセット」として扱います。つまり <TabIcon /> はコンポーネントではなく画像ソースのオブジェクトになり、コンポーネントとして描画しようとしても中身が生まれません。
Expo Go でたまたま表示されるのは、キャッシュや古いバンドル、あるいは別経路で読み込まれていたなど環境依存の偶然であることが多く、これに頼ると本番で崩れます。
なぜ表示されないのか
原因は大きく2つに分かれます。
ひとつ目は、.svg をコンポーネントとして import する仕組みが入っていないこと。React Native には Web の <img src="...svg"> のような仕組みがなく、SVGを描くには react-native-svg が必要です。さらに .svg ファイルをそのまま JSX コンポーネントとして扱うには react-native-svg-transformer を Metro に組み込む必要があります。これが無いと Metro は SVG を静的アセット扱いにしてしまいます。
ふたつ目は、色がSVGファイル内に固定で焼き込まれていること。デザインツールが書き出す SVG は fill="#111111" のように具体的な色を持っていることが多く、これだとアプリ側でテーマに応じて色を変えられません。ダークモードで黒いまま消えたように見えるのはこのパターンです。
解決策1: react-native-svg-transformer で .svg を import できるようにする
まず必要なパッケージを入れます。Expo 管理下では npx expo install を使うと SDK に合ったバージョンが入ります。
npx expo install react-native-svg
npm install --save-dev react-native-svg-transformer次にプロジェクト直下の metro.config.js を編集します。Expo の場合は getDefaultConfig を土台にするのが安全です。ファイルが無ければ新規作成します。
// metro.config.js
const { getDefaultConfig } = require("expo/metro-config");
const config = getDefaultConfig(__dirname);
// SVG を JSX コンポーネントとして扱えるようにする
config.transformer.babelTransformerPath = require.resolve(
"react-native-svg-transformer"
);
// .svg を「アセット」から外し、「ソースコード」として解決させる
config.resolver.assetExts = config.resolver.assetExts.filter(
(ext) => ext !== "svg"
);
config.resolver.sourceExts = [...config.resolver.sourceExts, "svg"];
module.exports = config;ポイントは assetExts から svg を除外し、sourceExts に加えるところです。この2行を忘れると transformer を入れても Metro は依然として SVG を画像として扱い続けます。
TypeScript を使っている場合は、.svg を import する型が無いと赤線が出ますので、declarations.d.ts のような型定義ファイルを1枚用意します。
// declarations.d.ts
declare module "*.svg" {
import type { SvgProps } from "react-native-svg";
const content: React.FC<SvgProps>;
export default content;
}最後に、設定を変えたら Metro のキャッシュを消してから再起動します。ここを飛ばすと古い解決結果が残り、直っているのに直っていないように見えます。
npx expo start --clearこれで先ほどの import TabIcon from "../assets/icons/home.svg" が、そのままコンポーネントとして描画されるようになります。
解決策2: currentColor でテーマに追従させる
表示はできても色が変わらない問題は、SVGの fill を固定色から currentColor に置き換えることで解決します。react-native-svg-transformer は SVG 内の currentColor を、コンポーネントに渡した color プロップに橋渡ししてくれます。
書き出された SVG のパスがこうなっていたら、
<svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<path fill="#111111" d="M12 3l9 7v11h-6v-6H9v6H3V10z" />
</svg>固定色を currentColor に書き換えます。
<svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<path fill="currentColor" d="M12 3l9 7v11h-6v-6H9v6H3V10z" />
</svg>呼び出し側では color を渡すだけで、テーマに応じて色が切り替わります。
import { useColorScheme } from "react-native";
import HomeIcon from "../assets/icons/home.svg";
export function HomeTab() {
const scheme = useColorScheme();
const tint = scheme === "dark" ? "#F2F2F7" : "#1C1C1E";
return <HomeIcon width={24} height={24} color={tint} />;
}複数の色を持つイラスト系SVGでは、変えたい部分だけ currentColor にして、変えたくない差し色は固定色のまま残す、という使い分けができます。アイコンのように単色で扱いたいものは全パスを currentColor に統一しておくと、後から配色を変える際に一箇所で済みます。
どちらの方法を選ぶか
| 状況 | 向いている方法 |
|---|---|
| デザイナー書き出しのSVGを大量に使う | 解決策1(transformer で import) |
| 数点だけ・動的に形を変えたい | react-native-svg の Svg / Path で手書き |
| 色をテーマに追従させたい | 解決策2(currentColor)を併用 |
私自身は、タブバーやリスト行の小さなアイコンは transformer で .svg を直接読み込み、色は currentColor に統一する形に落ち着きました。デザインの差し替えが .svg ファイルの入れ替えだけで済むので、運用がとても軽くなります。
予防策
同じ落とし穴を避けるために、私は次の3点を最初に固めるようにしています。
まず、プロジェクトを立ち上げた初日に metro.config.js と型定義を入れてしまうこと。後から入れると「なぜか表示されない」の切り分けに時間を取られます。
次に、SVGを追加したら必ず一度は 開発ビルド(Expo Go ではなく) で確認すること。Expo Go は同梱ライブラリの都合で挙動が異なることがあり、Expo Go で動いたことは本番で動く保証になりません。
最後に、設定を触ったら --clear を癖にすること。SVG 周りの「直したのに直らない」の大半は、Metro のキャッシュが古い解決結果を握っていることが原因でした。
参考リンク
- react-native-svg(公式リポジトリ)
- react-native-svg-transformer(公式リポジトリ)
- Expo: Using Metro(metro.config.js のカスタマイズ)
小さなアイコンひとつでも、表示の仕組みを理解しておくと後々の運用がぐっと楽になります。同じところで足止めされている方の助けになれば幸いです。お読みいただきありがとうございました。