A tiny React Native and Expo wrapper around Google's <model-viewer> web
component, rendered inside react-native-webview.
It is for simple GLB/glTF previews, not for building a native 3D engine.
Use this package when your product requirement sounds like:
"Show this
.glbin a React Native screen with orbit controls, auto-rotate, lighting, and load/error callbacks."
For that job, setting up Filament, Three.js, React Three Fiber, render loops,
lights, cameras, loaders, and Metro asset details can be more machinery than the
screen deserves. This package gives you the WebView version of the web
<model-viewer> ergonomics.
- Product cards or vehicle previews.
- Marketplace listings with one inspectable 3D asset.
- Expo demos and prototypes where WebView rendering is acceptable.
- Apps that already use
<model-viewer>on the web and want similar mobile behavior. - Teams that want a small dependency and do not need full scene control.
Use a native 3D stack instead when you need:
- native GPU rendering as the core experience
- custom materials, shaders, lights, or post-processing
- physics, collision, skeletal animation control, or multi-object scenes
- precise frame timing or heavy interaction
- AR or game-like rendering
Good alternatives:
react-native-filamentfor a native mobile rendering engine.@react-three/fiber/nativefor the Three.js ecosystem on React Native.- Expo
GLViewwhen you want lower-level WebGL control.
Expo:
npx expo install react-native-model-viewer-webview react-native-webviewBare React Native:
npm install react-native-model-viewer-webview react-native-webviewIf your app already has a compatible react-native-webview, install only
react-native-model-viewer-webview. Bare React Native apps should follow
react-native-webview's native setup instructions.
import { ModelViewerWebView } from "react-native-model-viewer-webview";
export function CarPreview() {
return (
<ModelViewerWebView
modelSource="https://example.com/car.glb"
style={{ height: 320 }}
htmlOptions={{
autoRotate: true,
cameraControls: true,
cameraOrbit: "35deg 66deg 6.2m",
disablePan: true,
exposure: 1.05,
shadowIntensity: 0.35,
}}
onModelLoaded={(status) => {
console.log(status.message);
}}
onModelError={(status) => {
console.warn(status.message);
}}
/>
);
}modelSource accepts:
- a remote URL
- a
data:URI - a
file:URI - a React Native static asset module number from
require(...) - an object with
localUriand/oruri, such as an Expo asset
<ModelViewerWebView
modelSource={require("./assets/car.glb")}
style={{ height: 320 }}
/>import { Asset } from "expo-asset";
import { ModelViewerWebView } from "react-native-model-viewer-webview";
const carModel = Asset.fromModule(require("./assets/car.glb"));
await carModel.downloadAsync();
<ModelViewerWebView modelSource={carModel} style={{ height: 320 }} />;localUri is preferred over uri when both exist. modelUri is still accepted
as a string-only compatibility prop.
If your app imports .glb files, add glb to Metro's asset extensions.
Expo example:
const { getDefaultConfig } = require("expo/metro-config");
const config = getDefaultConfig(__dirname);
config.resolver.assetExts = [
...config.resolver.assetExts,
"glb",
"gltf",
];
module.exports = config;If you test this package through file:../path, npm link, or a monorepo
symlink, make sure Metro resolves peer dependencies from the app, not from this
package's local node_modules.
Duplicate React copies can cause this error:
Invalid hook call. Hooks can only be called inside of the body of a function component.
In that setup, alias react, react-native, and react-native-webview to the
app's node_modules, and exclude any package-local dev copies of those peers
from Metro's file map.
By default, the generated WebView HTML inlines a vendored copy of
@google/model-viewer 4.2.0. That means the viewer runtime does not need a CDN
request, and local .glb previews can work offline when the model asset is also
available locally.
This makes the npm package larger, but keeps runtime behavior deterministic and
offline-friendly. The Apache-2.0 license for the bundled runtime is included in
vendor/model-viewer/LICENSE, with a summary in
THIRD_PARTY_NOTICES.md. The exact source package,
file hash, and generated runtime details are recorded in
vendor/model-viewer/SOURCE.md.
If you prefer a custom or CDN-hosted runtime, pass your own script URL:
import { MODEL_VIEWER_CDN_SCRIPT_URL } from "react-native-model-viewer-webview";
<ModelViewerWebView
modelSource="https://example.com/car.glb"
htmlOptions={{
modelViewerScriptUrl: MODEL_VIEWER_CDN_SCRIPT_URL,
}}
/>Or pass an inline module script if your build already bundles the source:
<ModelViewerWebView
modelSource="https://example.com/car.glb"
htmlOptions={{
modelViewerScript: bundledModelViewerSource,
}}
/>ModelViewerWebView accepts all react-native-webview props except source and
onMessage, which are managed by the package.
Important props:
| Prop | Type | Description |
|---|---|---|
modelSource |
string | number | { localUri?: string; uri?: string } |
Preferred model input. |
modelUri |
string |
String-only compatibility input. |
htmlOptions |
ModelViewerHtmlOptions |
Controls <model-viewer> attributes and script loading. |
onStatus |
(status, event) => void |
Receives every status message from the WebView. |
onModelLoaded |
(status, event) => void |
Fires when <model-viewer> emits load. |
onModelError |
(status, event) => void |
Fires when page or model loading fails. |
webViewBaseUrl |
string |
Base URL used for the generated HTML. Defaults to https://localhost/. |
Useful htmlOptions:
| Option | Description |
|---|---|
autoRotate |
Adds auto-rotate. |
cameraControls |
Adds camera-controls; defaults to true. |
cameraOrbit |
Sets the initial orbit. |
minCameraOrbit / maxCameraOrbit |
Bounds zoom/orbit. |
disablePan |
Adds disable-pan. |
exposure / shadowIntensity |
Basic visual tuning. |
backgroundColor / posterColor |
Controls the WebView and poster background. |
additionalAttributes |
Adds extra <model-viewer> attributes. |
modelViewerScriptUrl |
Overrides the bundled runtime with a custom <model-viewer> script URL. |
modelViewerScript |
Overrides the bundled runtime with a custom inline module script. |
See example/App.tsx for a minimal Expo screen.
- Public docs site
- How it works
- Compatibility and support policy
- AI agent usage
- Release and maintenance guide
- Contributing
- Security policy
- Changelog
The package includes agent-facing docs so coding agents can integrate it without guessing:
AGENTS.mdfor package-level agent instructionsllms.txtas a compact docs indexagent-skills/react-native-model-viewer-webview/SKILL.mdfor skills-compatible agents
Claude Code can use the skill after copying it to
~/.claude/skills/react-native-model-viewer-webview/SKILL.md or a project-local
.claude/skills/react-native-model-viewer-webview/SKILL.md.
Installing from npm does not automatically install a skill into an agent client.
See AI agent usage for copy/symlink instructions.
npm test
npm run typecheck
npm run pack:dry-runThe test suite runs against dist because that is the code npm consumers load.
When changing src, keep dist in sync before running the package tests.
This package renders through WebView, so it inherits WebView tradeoffs:
- startup cost is higher than a plain native view
- WebView memory use can matter in long lists
- native gestures and WebView gestures may need tuning in complex screens
- rendering behavior follows the platform WebView
The upside is simplicity. For many product preview surfaces, that is the whole point.
Before publishing a release:
- test a remote
.glbon Android - test a bundled
.glbon Android - test at least one iOS physical device if iOS support is claimed
- test a no-network/offline script setup if documenting offline support
- run
npm pack --dry-run - keep the README positioning honest: this is not a Filament/R3F replacement
MIT