Skip to Content
API ReferenceModelViewer

ModelViewer

The primary exported component. Renders a GLB, GLTF, OBJ, FBX, or USDZ model with orbit/pan/zoom controls, built-in side panels, and a full callback API.

Props

type Props = { modelUrl: string; modelFormat?: "glb" | "gltf" | "obj" | "fbx" | "usdz"; licenseKey: string; objectBindings: Record<string, ObjectBinding>; selectedObject?: ObjectBinding | null; onObjectBindingsChange?: (next: Record<string, ObjectBinding>) => void; onObjectSelect?: (binding: ObjectBinding | null) => void; onObjectHover?: (binding: ObjectBinding | null) => void; onModelLoaded?: (scene: Object3D) => void; onLoadError?: (error: unknown) => void; onAction?: (event: ObjectActionEvent) => void; onHiddenObjectsChange?: (next: Record<string, boolean>) => void; onCameraChange?: (camera: Camera, controls: CameraControls) => void; onViewerReady?: (viewer: ViewerReadyState) => void; onTextureUpload?: (file: File, objectId: string) => Promise<string>; onAnimationsReady?: (controls: AnimationControls) => void; onAnnotationsChange?: (annotations: AnnotationMarker[]) => void; activeAnnotation?: AnnotationMarker | null; onActiveAnnotationChange?: (annotation: AnnotationMarker | null) => void; lights?: React.ReactNode; camera?: React.ComponentProps<typeof Canvas>["camera"]; backgroundColor?: string; shadows?: boolean; showObjectBindingDataPanel?: boolean; customObjectBindingDataPanel?: ( props: CustomObjectBindingDataPanelProps, ) => React.ReactNode; customSceneObjectsPanel?: ( props: CustomSceneObjectsPanelProps, ) => React.ReactNode; showSceneObjectsPanel?: boolean; showDownloadButton?: boolean; downloadFilename?: string; showResetButton?: boolean; showLoadingOverlay?: boolean; showMouseController?: boolean; mouseControllerPosition?: | "bottom-left" | "bottom-right" | "top-left" | "top-right" | "center" | "center-bottom" | "center-top"; mouseControllerOpacity?: number; moveSensitivity?: number; zoomSensitivity?: number; sceneConfig?: SceneConfig; disableZoom?: boolean; zoomOnSelected?: boolean; enableCameraControls?: boolean; moveModeEnabled?: boolean; objectTransformSpace?: "local" | "world"; enableKeyboardNavigation?: boolean; onAutoFit?: () => Promise<boolean>; refitOnResize?: boolean; renderMode?: "always" | "demand"; maxDpr?: number; performanceProfile?: "auto" | "high" | "low"; dracoDecoderPath?: string | false; ktx2TranscoderPath?: string | false; meshopt?: boolean; showMeasureTools?: boolean; showUvCheckerButton?: boolean; showExplodeControls?: boolean; cinematic?: boolean | CinematicConfig; measurementUnit?: string; enableXR?: boolean; usdzUrl?: string; showAnnotationNavigation?: boolean; showAnnotationOnHover?: boolean; showViewGizmo?: boolean; };

modelUrl

Required. URL or public path to a .glb, .gltf, .obj, .fbx, or .usdz model. Hosted assets must serve external buffers, MTL files, and textures at their declared relative paths; USDZ dependencies are packaged internally.

<ModelViewer modelUrl="/model.glb" ... />

modelFormat

Optional explicit format for extensionless or signed URLs. Supported file extensions are detected automatically in normal URLs.

<ModelViewer modelUrl={signedUrl} modelFormat="fbx" ... />

This is separate from usdzUrl: modelUrl renders USDZ inside the viewer, while usdzUrl launches the native Apple Quick Look experience when XR is enabled.


licenseKey

Required. License key for the library.

<ModelViewer licenseKey="your-license-key" ... />

To get a license key, sign in at the React Immersive Console , choose a plan, and generate a key from your dashboard.


objectBindings

Required. A map keyed by the model node name. Each key should match a mesh name from the GLB/GLTF file.

objectBindings is the single source of truth for all visual state:

FieldEffect
visibleControls mesh visibility
style.material.baseColorSets mesh color (committed by the built-in color picker on close)
style.material.texture.pathSets the base color (albedo) texture
style.material.*Per-object MeshPhysicalMaterial overrides, see below
cameraState.position / cameraState.targetCamera view to use when this object is focused or selected from the viewer UI

All visual overrides live under style.material (an ObjectBindingMaterial). It covers base color/PBR, emission, opacity & blending, normal/bump, displacement, ambient occlusion, clearcoat, sheen, anisotropy, transmission/volume, and face sidedness, plus a texture-map slot for each. See ObjectBindingMaterial for the full field list and the per-slot color-space handling (color maps are decoded as sRGB, data maps stay linear).

const objectBindings = { Object_2: { id: "obj-2", modelObjectId: "Object_2", type: "body", visible: true, cameraState: { position: [2.8, 1.6, 4.2], target: [0, 0.8, 0], }, style: { material: { baseColor: "#ff0000", texture: { path: "/materials/body-finish.jpg" }, roughness: 0.4, clearcoat: 1, }, }, actions: [ { id: "change-color", label: "Change Color", type: "command" }, { id: "toggle-visibility", label: "Toggle Visibility", type: "command" }, ], metrics: {}, metadata: {}, }, };

selectedObject

Optional currently selected object binding, or null when nothing is selected.

When provided, ModelViewer behaves as a controlled component. When omitted, it manages its own selection state internally.

When the selected object has a cameraState with both position and target, the viewer uses that saved camera view for focus/select behavior instead of falling back to fitToBox.


onObjectSelect

Called when the user clicks a mesh or closes the side panel.

onObjectSelect?: (binding: ObjectBinding | null) => void
  • Clicking a mesh → onObjectSelect(binding)
  • Closing the panel → onObjectSelect(null)
  • Clicking the same mesh again still fires; selection is not toggled off automatically
  • If the binding includes cameraState.position and cameraState.target, the viewer moves the camera to that saved view when focusing that object
  • Clicks resolve the front-most surface under the pointer; a mesh whose binding has selectable: false does not pass the click through to meshes behind it
  • On very large models, clicking meshes in the viewport is disabled entirely, see Large models. Selection through the scene objects panel (and this callback) still works

onObjectHover

Called when the user hovers a mesh.

onObjectHover?: (binding: ObjectBinding | null) => void
  • Pointer over a mesh → onObjectHover(binding)
  • Pointer out → onObjectHover(null)
  • On very large models, viewport hover is disabled entirely, see Large models

onObjectBindingsChange

Fired when the viewer’s built-in UI updates binding data. Wire this back into your state to keep the viewer and your app in sync.

onObjectBindingsChange?: (next: Record<string, ObjectBinding>) => void

Fires for:

  • Visibility toggles (binding.visible)
  • Color picks (binding.style.material.baseColor), committed when the color picker closes
  • Texture uploads (binding.style.material.texture.path)
  • Texture removal
  • Object movement and rotation (binding.transform.position and binding.transform.rotation)

Passing this callback makes binding edits controlled: update the objectBindings prop with the returned record. If it is omitted, the viewer keeps built-in edits in internal state for the lifetime of the mounted viewer.


onHiddenObjectsChange

Fired with the derived hidden-object map whenever binding visibility state changes.

onHiddenObjectsChange?: (next: Record<string, boolean>) => void

onModelLoaded

Called after the GLB/GLTF scene has been loaded.

onModelLoaded?: (scene: Object3D) => void

onLoadError

Called if the model fails to load or render.

onLoadError?: (error: unknown) => void

onAction

Called when a built-in action button is clicked from the side panel.

type ObjectActionEvent = { objectId: string; action: ObjectBindingAction; binding?: ObjectBinding; screenX?: number; screenY?: number; }; onAction?: (event: ObjectActionEvent) => void

onViewerReady

Called once camera controls are available and again when the viewer publishes updated ready-state data (for example after model load, camera changes, or binding changes).

onViewerReady?: (viewer: ViewerReadyState) => void type ViewerReadyState = { controls: CameraControls; scene: Object3D | null; objectBindings: Record<string, ObjectBinding>; nodeRefs: Record<string, Object3D>; captureImage: (options?: CaptureImageOptions) => Promise<string>; }; type CaptureImageOptions = { width?: number; height?: number; transparent?: boolean; };

captureImage forces a render and resolves with a data:image/png URL of the canvas. Pass width/height to render at a resolution other than the canvas’s current size, and transparent to hide the configured background so the PNG has an alpha channel. It rejects if called before the viewer is ready.

onViewerReady={(viewer) => { viewer .captureImage({ width: 1920, height: 1080, transparent: true }) .then((dataUrl) => { // upload, preview, etc. }); }}

For turning the current camera/visibility/selection into a shareable link, see useShareableViewerState.


onCameraChange

Called whenever the camera controls update.

onCameraChange?: (camera: Camera, controls: CameraControls) => void

onTextureUpload

Optional async callback for handling texture file uploads. Use it to upload to your own storage (S3, Cloudinary, CDN, etc.) and return a durable URL.

onTextureUpload?: (file: File, objectId: string) => Promise<string>

When omitted, the viewer falls back to URL.createObjectURL(file), a session-scoped blob URL that is lost on reload.

<ModelViewer onTextureUpload={async (file, objectId) => { const formData = new FormData(); formData.append("file", file); formData.append("objectId", objectId); const res = await fetch("/api/upload-texture", { method: "POST", body: formData, }); const { url } = await res.json(); return url; }} />

onAnimationsReady

Called after the GLB/GLTF model is loaded, providing animation controls. Called even if the model has no animations (empty clips array, no-op functions).

type AnimationControls = { clips: string[]; clipDetails?: { sourceName: string; duration: number }[]; play: (clipName: string) => void; pause: () => void; stop: () => void; setSpeed: (speed: number) => void; seek?: (time: number) => void; // scrub the current clip to an absolute time (seconds) getState?: () => AnimationPlaybackState; // includes live time + duration subscribe?: (listener: (state: AnimationPlaybackState) => void) => () => void; }; onAnimationsReady?: (controls: AnimationControls) => void

See AnimationControls for the full AnimationPlaybackState shape (currentClip, isPlaying, speed, time, duration).

Wire to useViewerAnimations().handleAnimationsReady for the simplest integration.


lights

Optional custom lighting rendered inside the scene. When omitted, ModelViewer uses its default light rig.

function CustomLights() { return ( <> <ambientLight intensity={0.5} /> <directionalLight position={[4, 8, 4]} intensity={1.6} castShadow /> <pointLight position={[-3, 3, 2]} intensity={0.8} /> </> ); } <ModelViewer lights={<CustomLights />} ... />

camera

Optional camera configuration passed through to the underlying React Three Fiber Canvas.

ModelViewer only sets a default fov: 50, it does not set a default position, so an unset position falls back to React Three Fiber’s own Canvas default ([0, 0, 5]). Pass an explicit position for predictable framing.

<ModelViewer camera={{ position: [0, 2.2, 7], fov: 40, near: 0.1, far: 1000 }} ... />

Passing an explicit camera counts as choosing the initial view yourself, so the mount-time auto-fit (see onAutoFit) is skipped automatically and your position/fov stick. Provide onAutoFit as well only if you want to run your own fit-to-scene logic instead.


backgroundColor

Optional background color for the viewer canvas.

<ModelViewer backgroundColor="#0f172a" ... />

shadows

Controls whether the viewer renders shadows. Default: true.

When true, the canvas renders soft shadows: the default light rig casts a directional key-light shadow, furniture/decor meshes cast and receive shadows, and ambient occlusion is applied via post-processing. When false, shadow-map rendering is disabled on the canvas, no shadow-pass cost, and per-mesh/light castShadow flags are ignored.

<ModelViewer shadows={false} ... />

Interior models (rooms, dollhouses): the large mesh that forms the walls/ceiling enclosure is detected by its bounding-box size and automatically excluded from casting shadows (it still receives them). Without this, a closed shell would block a top-down light and seal the interior in darkness. Furniture and decor then cast realistic contact shadows onto the floor. Pass a custom lights rig to fully override the default lighting.


showObjectBindingDataPanel

Controls whether the built-in left object details panel is rendered. Default: true.


customObjectBindingDataPanel

Render prop for replacing the built-in left object details panel.

type CustomObjectBindingDataPanelProps = { isOpen: boolean; selectedObject: ObjectBinding | null; currentAction: ObjectActionEvent | null; onClose: () => void; onAction: (event: ObjectActionEvent) => void; };

showSceneObjectsPanel

Controls whether the built-in right scene objects panel is rendered. Default: true.


customSceneObjectsPanel

Render prop for replacing the built-in right scene objects panel.

type CustomSceneObjectsPanelProps = { objectBindings: Record<string, ObjectBinding>; onAction?: (event: ObjectActionEvent) => void; onFocus?: (binding: ObjectBinding) => void; onHover?: (binding: ObjectBinding | null) => void; };

showDownloadButton

Enables the download split-button (export GLB, export PNG screenshot, or export the model’s UV layout). Default: true.

UV export downloads an editable 2048×2048 SVG UV-island outline when the model uses one material. For multi-material models it downloads a ZIP containing one SVG texture template per material. Meshes without a uv attribute are omitted; when the model has no UV coordinates, the viewer reports that no layout is available.

The bottom-right action bar renders when at least one of showResetButton, showDownloadButton, or (showAnnotationNavigation with annotations present) is true.


downloadFilename

Filename stem for the built-in model export button. Default: "model".


showResetButton

Shows the “reset camera view” button in the bottom-right action bar. Default: true.


showLoadingOverlay

Controls whether the built-in loading overlay is shown while the model is loading and the initial camera fit is settling. Default: true.


showMouseController

Renders an on-screen joystick/zoom controller for moving the camera with a mouse or touch, useful on touch devices or kiosk layouts. Default: false.

<ModelViewer showMouseController mouseControllerPosition="bottom-right" mouseControllerOpacity={0.8} moveSensitivity={0.1} zoomSensitivity={1.2} ... />

mouseControllerPosition

Placement of the on-screen controller. Default: "center-bottom".

type MouseControllerPosition = | "bottom-left" | "bottom-right" | "top-left" | "top-right" | "center" | "center-bottom" | "center-top";

mouseControllerOpacity

Opacity of the on-screen controller. Default: 1.


moveSensitivity

Movement sensitivity for the on-screen controller. Default: 0.08.


zoomSensitivity

Zoom sensitivity for the on-screen controller. Default: 1.0.


sceneConfig

Optional scene-wide configuration object, model, camera, lighting, wireframe, shadows, environment, background, ground shadows, post-processing, animations, annotations, and a cinematic camera path. This is the same SceneConfig shape authored by BindingBuilder’s Scene tab. If omitted, the viewer uses its built-in default scene config.

Use it to define the experience around the model, rather than individual model objects. For example, a guided tour can use annotations and saved world positions to direct the camera through a scene without requiring objectBindings for every tour stop. See the Guided Tour example.

sceneConfig.animations drives autoplay and per-clip playback (see onAnimationsReady), and sceneConfig.annotations seeds the annotation markers rendered on the model.

sceneConfig.model.renderer accepts "pbr", "matcap", or "uv-checker". UV Checker temporarily replaces every mesh material with an unlit numbered test chart without changing objectBindings or uploaded textures. Switching back restores the original material references. Meshes without UV0 coordinates appear magenta and trigger an explanatory notice.

const { sceneConfig, updateSceneConfig } = useSceneConfig(initialSceneConfig); const enableUvChecker = () => updateSceneConfig({ model: { renderer: "uv-checker" } }); <ModelViewer {...props} sceneConfig={sceneConfig} /> <button onClick={enableUvChecker}>Show UV checker</button>

See useSceneConfig for nested scene updates and the pure patchSceneConfig alternative.


disableZoom

Optional boolean. When true, the viewer never zooms the camera to an object on selection (selection still highlights and fires callbacks). Default: false.


zoomOnSelected

Optional boolean controlling whether selecting an object zooms/fits the camera to it. Set to false to keep the current camera framing on selection. Default: true. (Zooming is also skipped when disableZoom is true.)


Moving and rotating objects

Set moveModeEnabled to show a centered PivotControls gizmo for the selected bound object. Its translation arrows, plane handles, and rotation rings are available at the same time; scaling is disabled.

import { useState } from "react"; import { ModelViewer, type ObjectBinding, } from "@liveroom-tech/react-immersive"; export function EditableViewer({ initialBindings, }: { initialBindings: Record<string, ObjectBinding>; }) { const [bindings, setBindings] = useState(initialBindings); return ( <ModelViewer modelUrl="/model.glb" licenseKey="your-license-key" objectBindings={bindings} onObjectBindingsChange={setBindings} moveModeEnabled objectTransformSpace="world" /> ); }

Click a bound mesh to select it. Drag an arrow to move on one axis, a plane handle to move on two axes, or a ring to rotate. The pivot is calculated from the center of the object’s renderable geometry, so the object rotates around itself even when its authored node origin is elsewhere.

objectTransformSpace accepts:

  • "world" (default): handles stay aligned with the scene axes
  • "local": handles follow the selected object’s current orientation

Changing axis space only changes handle alignment; it does not move the object. Changes are persisted to the binding as local-space transform.position and transform.rotation. Positions use model units and rotations use radians.

If you pass selectedObject, selection is controlled too. Update it from onObjectSelect. If you omit both props, ModelViewer manages selection internally.


enableCameraControls

Optional boolean controlling orbit, pan, and zoom input. Default: true. Camera controls are paused whenever moveModeEnabled is on so pointer drags manipulate the object.


enableKeyboardNavigation

Optional boolean enabling canvas-focused camera shortcuts. Default: false. Click or focus the canvas first, then use W/S to move forward/back, A/D to truck left/right, Space/C to move up/down, and the arrow keys to orbit. Shift + Up/Down moves forward/back. The shortcuts are inactive while object editing or camera controls are disabled.


showViewGizmo

Optional boolean showing a clickable camera-orientation gizmo in the top-right of the viewer. Default: false. It displays the scene axes and can snap the camera to an axis. This is separate from the selected object’s transform gizmo and is hidden while moveModeEnabled is on.


onAutoFit

Optional async callback invoked once the model has loaded and the scene is ready. When provided, it is called so consumers can run their own fit-to-scene logic (e.g. an animated fit); if omitted, the viewer falls back to its internal fitScene, unless an explicit camera prop was given, in which case that position is treated as the chosen initial view and fitScene is skipped.

onAutoFit?: () => Promise<boolean>

refitOnResize

Optional boolean controlling whether the camera re-frames the model to fit whenever the canvas is resized, a browser window resize, a device rotation, or a side panel opening/closing (which changes how much width the canvas has). Set to false to preserve the user’s current orbit/zoom across resizes; the camera aspect stays correct either way, so the model never distorts, it just isn’t re-centered. Only affects re-fits after the initial mount-time auto-fit. Default: true.


onAnnotationsChange

Optional callback fired when the annotation markers on the model change (added, edited, or removed through the viewer UI).

(annotations: AnnotationMarker[]) => void

AnnotationMarker is the SceneAnnotationMarker shape:

type AnnotationMarker = { id: number; worldPosition: [number, number, number]; localPosition: [number, number, number]; title: string; description: string; };

activeAnnotation / onActiveAnnotationChange

Optional controlled state for which annotation is currently open. Pass activeAnnotation to control it from your app, and onActiveAnnotationChange to be notified when the viewer wants to open (AnnotationMarker) or close (null) one. If activeAnnotation is left undefined, the viewer manages this state internally (uncontrolled).

activeAnnotation?: AnnotationMarker | null; onActiveAnnotationChange?: (annotation: AnnotationMarker | null) => void;

renderMode

"demand" (default) only re-renders the canvas when something changes (camera move, state update, animation frame); "always" runs a continuous render loop. Leave at "demand" for lower GPU/battery cost unless you have a custom scene that mutates outside React/animation state.


maxDpr

Upper bound for the device pixel ratio used when rendering, so retina/4K displays don’t render at full 2–3x cost. Default: 2.


performanceProfile

Optional control over how aggressively the viewer trades visual fidelity for a stable WebGL context on constrained GPUs.

performanceProfile?: "auto" | "high" | "low";
  • "auto" (default) applies a reduced profile on handheld/mobile browsers and other low-power touch devices: the postprocessing pipeline is skipped, soft shadows are disabled, and the device pixel ratio is capped. Desktop-class touch devices may still keep the higher-quality path when they advertise plenty of memory.
  • "high" always renders at full quality.
  • "low" always applies the reduced profile.

Default: "auto".


dracoDecoderPath / ktx2TranscoderPath / meshopt

Control the compressed-asset decoders used when loading the GLB/GLTF asset.

dracoDecoderPath?: string | false; ktx2TranscoderPath?: string | false; meshopt?: boolean;

DRACO, Meshopt, and KTX2 decoding are enabled by default via hosted decoder/transcoder bundles, fetched lazily only when the model actually needs them. Pass a path to self-host the decoder/transcoder files, or false to disable that decoder entirely.


showUvCheckerButton

Shows a UV Checker toggle button over the canvas. Turning it on temporarily replaces the model’s materials with the numbered diagnostic texture; turning it off restores the renderer configured by sceneConfig. Meshes without UV0 coordinates appear magenta. Default: false.

<ModelViewer showUvCheckerButton ... />

showMeasureTools

Shows a click-to-measure toolbar over the canvas: click two points on the model to get a distance readout, plus a toggleable bounding-box dimensions overlay (width/height/depth). Default: false.

<ModelViewer showMeasureTools measurementUnit="m" ... />

measurementUnit

Unit suffix appended to measurement readouts. glTF models are authored in meters by spec, so values are not converted, this only changes the displayed label. Default: "m".


showExplodeControls

Shows an exploded-view control over the canvas: a slider that slides each bound part outward from the model center to reveal interior/assembly structure, then back to reassemble it. Best suited to static product/CAD models, the offset writes node positions each frame, so it conflicts with a playing skeletal animation. Default: false.

<ModelViewer showExplodeControls ... />

See the Exploded View example.


cinematic

Enables the cinematic auto-camera: the camera glides along a path on its own, like a film, ideal for showing off a gallery, apartment, or product with no user interaction. Renders a play/pause button over the canvas; grabbing the camera (or selecting a part) pauses it and hands control back.

cinematic?: boolean | CinematicConfig; type CinematicConfig = { waypoints?: CinematicWaypoint[]; // camera keyframes to glide through duration?: number; // seconds for one full pass (auto-scales when omitted) loop?: boolean; // default true autoPlay?: boolean; // start once the model has loaded and framed (default false) }; type CinematicWaypoint = { position: [number, number, number]; target: [number, number, number]; // world-space look-at point azimuthAngle?: number; // preserves orbit rotation at vertical poles polarAngle?: number; };
  • Pass cinematic as boolean true (or an object with fewer than two waypoints) to get a zero-config showcase orbit around the model’s bounding sphere.
  • Pass two or more waypoints to author a walkthrough: the camera follows a smooth Catmull-Rom spline through them at a constant on-screen speed, looping seamlessly.
  • Waypoints are easiest to capture visually in BindingBuilder’s Cinematic tab (frame a view, click Add waypoint), which stores them on sceneConfig.cinematic.

Precedence. A path can also come from sceneConfig.cinematic (authored in BindingBuilder). The cinematic prop overwrites it: a boolean toggles the feature (true keeps any authored waypoints, false forces it off), and an object overrides field-by-field, so you can keep the authored waypoints while overriding, say, autoPlay or loop.

// Zero-config showcase orbit <ModelViewer cinematic ... /> // Authored walkthrough <ModelViewer cinematic={{ waypoints: [ { position: [4, 2, 6], target: [0, 1, 0] }, { position: [-3, 2, 4], target: [0, 1, 0] }, { position: [-5, 2, -2], target: [0, 1, 0] }, ], loop: true, autoPlay: true, }} ... /> // Keep the BindingBuilder-authored path, just autoplay it <ModelViewer sceneConfig={sceneConfig} cinematic={{ autoPlay: true }} ... />

Default: disabled.


enableXR

Optional boolean that enables XR entry points. On WebXR-capable browsers, the viewer shows an AR and/or VR button once the relevant immersive session support is confirmed. On iPhone/iPad, WebXR AR is not available, but if you also pass usdzUrl, the viewer shows a “View in AR” button that launches Apple Quick Look instead.

Default: false.


usdzUrl

Optional USDZ URL used as an Apple-device AR fallback when enableXR is true.

usdzUrl?: string;
  • Point this to a .usdz file Safari can fetch directly.
  • iPhone/iPad will show a “View in AR” button that launches Apple Quick Look when WebXR AR is unavailable.
  • Android / WebXR-capable devices ignore this prop and continue using the normal WebXR AR flow.

arScaleMode

Controls the initial model size in WebXR AR and VR sessions.

arScaleMode?: "normalized" | "real-world";
  • "normalized" (default) fits the longest model dimension to approximately 1 metre. Use this for arbitrary uploads and room-scale scenes.
  • "real-world" preserves the GLB’s authored scale. glTF units are metres, so use this for product models exported at their real dimensions.

Users can still pinch to resize after placement in either mode.


mobileHandoffUrl

Optional public URL used for the desktop-to-phone AR handoff. When enableXR is true, desktop viewers show an “Open on phone” QR-code control. Scan it to open the experience on a phone, then use its AR control (or iOS Quick Look).

mobileHandoffUrl?: string;
  • Omit it to encode the current page URL, including its hash. This works with useShareableViewerState so the phone opens the same camera/visibility/selection state.
  • Pass a dedicated HTTPS mobile-viewer URL when desktop and mobile use different routes.
  • The QR handoff is hidden on phones and tablets because they can enter AR directly.
  • The target must be publicly reachable from the phone; localhost URLs will not work.

showAnnotationNavigation

Shows the “Guided Tour” control cluster (left side of the bottom action bar) when the model has at least one annotation: a single “Guided Tour” button that, once started, becomes Previous/Stop/Next controls for stepping through annotations in order. Default: true.


showAnnotationOnHover

Temporarily opens an annotation’s detail popup when the pointer hovers over its marker. Moving the pointer away closes the hover popup after a short grace period. Clicking a marker still locks its popup open until it is closed. Default: false.

<ModelViewer {...props} showAnnotationOnHover />

Large models

The viewer accelerates viewport picking by building a BVH (bounding volume hierarchy) index for the loaded model in a background worker. Models above roughly 12 million triangles are too large to index without a significant memory cost, so the viewer degrades gracefully instead:

  • Viewport pointer interaction is disabled: clicking or hovering meshes in the canvas does nothing, and onObjectSelect / onObjectHover do not fire from the viewport. Selection through the scene objects panel still works and still fires onObjectSelect.
  • A dismissible banner appears over the canvas telling the user the model is very large, that it may render slowly on their device, and that selection is available through the panel.
  • The loading overlay narrates the extra work (“Preparing large model…”), uploading that much geometry to the GPU is a single synchronous operation that can block the page for tens of seconds, on any WebGL viewer.

Orbit, zoom, pan, animations, annotations, and the panels all keep working.

There is no way to make a model of this size feel fast in a browser, the per-frame cost of tens of millions of triangles is inherent, and on mobile devices the decoded geometry alone can exceed the tab’s memory budget. For assets like this, decimate at ingestion (for example to ~2–3M triangles) rather than shipping the raw export; a decimated model loads in seconds and keeps full interactivity. Note that Blender or native tooling is required for very large Draco-compressed files, the WASM builds of gltf-transform and gltfpack cannot decode them within their 4GB address space.


Theming

The scene objects panel and object binding data panel read their colors, borders, shadows, and fonts from CSS custom properties instead of hardcoded values. Set them on a .ri-viewer ancestor (or any element wrapping the viewer) to re-skin those panels without overriding individual classes, every variable already has the current default baked in as a fallback, so you only need to set the ones you actually want to change.

.my-app .ri-viewer { --ri-sidepanel-bg: #1a0033; --ri-sidepanel-accent: #ff2d75; --ri-sidepanel-accent-soft: rgba(255, 45, 117, 0.25); --ri-sidepanel-text: #ffe8f5; }
<div className="my-app"> <ModelViewer ... /> </div>

Scene objects panel, --ri-sidepanel-*

VariableDefault
--ri-sidepanel-font-familyInter, "Segoe UI", Helvetica, Arial, sans-serif
--ri-sidepanel-bg#141824
--ri-sidepanel-open-btn-bgrgba(20, 20, 20, 0.82)
--ri-sidepanel-open-btn-bg-hover#27282b7e
--ri-sidepanel-borderrgba(255, 255, 255, 0.06)
--ri-sidepanel-dividerrgba(255, 255, 255, 0.04)
--ri-sidepanel-border-strongrgba(255, 255, 255, 0.08)
--ri-sidepanel-border-activergba(255, 255, 255, 0.18)
--ri-sidepanel-shadow-left4px 0 24px rgba(0, 0, 0, 0.35)
--ri-sidepanel-shadow-right-4px 0 24px rgba(0, 0, 0, 0.35)
--ri-sidepanel-text#f1f5f9
--ri-sidepanel-text-emphasis#ffffff
--ri-sidepanel-text-muted#94a3b8
--ri-sidepanel-text-subtle#64748b
--ri-sidepanel-placeholder#475569
--ri-sidepanel-surfacergba(255, 255, 255, 0.05)
--ri-sidepanel-surface-hoverrgba(255, 255, 255, 0.1)
--ri-sidepanel-accent#3b82f6
--ri-sidepanel-accent-softrgba(59, 130, 246, 0.12)

Object binding data panel, --ri-databinding-*

VariableDefault
--ri-databinding-bg#111318
--ri-databinding-borderrgba(255, 255, 255, 0.06)
--ri-databinding-border-strongrgba(255, 255, 255, 0.08)
--ri-databinding-border-hoverrgba(255, 255, 255, 0.15)
--ri-databinding-shadow4px 0 24px rgba(0, 0, 0, 0.35)
--ri-databinding-font-family"Inter", "Segoe UI", sans-serif
--ri-databinding-mono-font-family"JetBrains Mono", "Fira Code", "Menlo", monospace
--ri-databinding-text#f1f5f9
--ri-databinding-text-muted#94a3b8
--ri-databinding-text-subtle#64748b
--ri-databinding-text-faint#475569
--ri-databinding-text-hover#cbd5e1
--ri-databinding-surfacergba(255, 255, 255, 0.03)
--ri-databinding-surface-hoverrgba(255, 255, 255, 0.06)
--ri-databinding-accent#93c5fd
--ri-databinding-accent-glowrgba(147, 197, 253, 0.8)
--ri-databinding-accent-borderrgba(96, 165, 250, 0.55)
--ri-databinding-accent-gradientlinear-gradient(135deg, #1e3a8a 0%, #1d4ed8 60%, #2563eb 100%)
--ri-databinding-accent-gradient-activelinear-gradient(135deg, #1e3a8a 0%, #1e40af 100%)

ModelViewer.css also declares the original --ri-panel-* and --ri-license-overlay-* variables for its other surfaces, the two sets above are specifically for the scene objects panel and the object binding data panel.


Built-In Actions

Action IDBehavior
toggle-visibilityHides or shows the selected mesh via binding.visible + onObjectBindingsChange
change-colorOpens the color picker inline under the action; hex committed to binding.style.material.baseColor on close
change-materialOpens the texture upload panel inline under the action; URL stored in binding.style.material.texture.path