SVG Assets Don't Render in React Native's <Image> on Native
Summary
The BSL On Tour event hero banner, its "Explore All Events" list
thumbnails, and the dashboard carousel's logo slides all rendered
correctly on web but showed blank (just the surrounding solid-color
background, no logo/graphic) on native Android/iOS. Not a crash, no
error — the image simply never painted. Root cause: several BSL-only
image assets (bsl-ontour-pro.svg, bsl-peru-pro.svg,
bsl-chile-pro.svg, bsl-colombia-pro.svg,
BSL-Logo-fondo-oscuro-2024.svg, bsl2025-hero.svg) were require()'d
directly and passed straight into React Native's <Image source={...}>.
Why this only broke on native
React Native's Image component is backed by the platform's native image
view (Android ImageView / iOS UIImageView), and neither platform's
built-in image decoder understands SVG — they only decode raster formats
(PNG, JPEG, WebP, GIF, BMP). On web, react-native-web maps Image to a
plain <img> DOM element, and every browser renders SVG natively via
<img src="foo.svg">. Same source code, same require(), genuinely
different rendering behavior per platform — no error surfaces on native
because passing an unsupported/undecodable URI to Image just fails to
paint, silently.
This project had already hit and fixed this exact class of bug once,
for the small HASHPASS wordmark logo — see
apps/mobile-app/lib/hashpass-logo.ts, which splits each logo into a
_WEB (.svg) and _NATIVE (.webp) variant, resolved via
Platform.OS === "web". The BSL-only assets never got the same
treatment because no raster counterpart existed for them yet — the
.svg requires in apps/mobile-app/lib/event-branding.ts and
apps/mobile-app/components/EventBannerCarousel.tsx were simply the
only option available, and shipped anyway since nobody caught it on web
during review.
HASHPASS landing hero: use the visible foreground, not the filename
The HASHPASS wordmark has a legacy naming divergence that is easy to get wrong:
logo-full-hashpass-black.svgrenders white letters with a red mark. It is the canonical web asset for the dark landing hero.logo-full-hashpass-black.webprenders dark letters with a red mark. It is suitable for a light surface, not the landing hero.logo-full-hashpass-white-cyan.webprenders white letters with a cyan mark. It is the compatible native fallback because React Native's plainImagecannot decode SVG.
Do not select these files from their historical black/white names. Use
the semantically named exports in apps/mobile-app/lib/hashpass-logo.ts.
getHashpassStaticHeroLogo() deliberately returns the verified
logo-full-hashpass-black.svg on web and the white-letter raster fallback on
Android/iOS, regardless of the surrounding application theme: the landing
hero itself is always dark.
The fix (2026-07-29)
Rasterized all six affected SVGs with sharp (already a repo
dependency; its webp()/png() output methods handle SVG input via
librsvg) at native resolution, alpha channel preserved:
const sharp = require('sharp');
await sharp('assets/logos/bsl/bsl-ontour-pro.svg').webp({ lossless: true }).toFile('assets/logos/bsl/bsl-ontour-pro.webp');
await sharp('assets/logos/bsl/bsl-ontour-pro.svg').png().toFile('assets/logos/bsl/bsl-ontour-pro.png');
Repointed the require() calls in event-branding.ts and
EventBannerCarousel.tsx's duplicate top-level consts to the new
.webp files. Unlike hashpass-logo.ts, no _WEB/_NATIVE platform
branch was introduced here — these consts are plain module-level values
consumed by several call sites (resolveEventImageSource,
getTourBrandAsset, getLampBrandConfig, HASHPASS_BRAND_LOGOS)
without any isWeb parameter in their signatures, and .webp renders
correctly on web too (just a marginally larger payload than an
equivalent .svg), so branching would have added complexity without a
real benefit here. Reserve the _WEB/_NATIVE split for cases where a
function already takes a platform-relevant parameter.
The .svg string keys in EVENT_IMAGE_ASSETS (e.g.
'/assets/logos/bsl/bsl-ontour-pro.svg') were not changed — those
are lookup identifiers matched against whatever path string the event
data supplies, independent of which file the corresponding
require() actually resolves to.
Verified via a real expo export --platform web that the built bundle
resolves the new .webp assets (confirmed the hashed output filename
matches, e.g. bsl-ontour-pro.8fc2f93c785298ed1a7ed070649b0939.webp) —
the .svg strings still present in the bundle afterward are exactly
the unchanged EVENT_IMAGE_ASSETS lookup keys, not a sign the fix
didn't take.
How to avoid this class of bug going forward
- Never
require()a raw.svgfor a plain<Image>source. Either rasterize it to.webp/.pngfirst, or render it withreact-native-svg's<SvgUri>/<SvgXml>components instead (seeapps/mobile-app/components/explorer/ExplorerHeader.tsx, which correctly usesSvgUriand was unaffected by this bug). - A blank image region with no console error, present on native but
correct on web, is the specific signature of this bug — check for a
.svgrequire()feeding a plainImagebefore assuming it's a data/network/permission issue. - Web-only review is not sufficient signal that an image asset works — this bug shipped because it looked fine in every web preview.