At Target I worked on a full-stack app that was, essentially, an internal Dropbox for
3D assets — the models the design and merchandising teams use to stage products in
virtual scenes. Teams needed one place to upload, browse, preview, and share .glb files without emailing hundred-megabyte binaries around.
My main piece was the file upload component. Beyond taking the upload, it generated a thumbnail for each model so the browse grid didn’t have to load a full 3D scene per tile, and it rendered an interactive preview of the asset itself. Around that were the usual full-stack pieces: browse and detail pages in React, and backend endpoints for upload, listing, metadata, and serving the files.
The preview and thumbnailing both leaned on the same tool, and it’s genuinely one of
the nicest ways to put a 3D model on a page: Google’s <model-viewer>. This post is a quick, practical intro.
What model-viewer is
<model-viewer> is a web component — a custom HTML element. You give it a .glb or .gltf file and you get an interactive, camera-controllable, optionally
auto-rotating 3D viewer with sensible defaults (lighting, shadows, even AR on
supported phones). Because it’s a plain element and not a React component, it drops
into React, Svelte, or nothing-but-HTML the exact same way.
Here it is running right now, pointed at the NeilArmstrong model from model-viewer’s own demo assets — drag to orbit, scroll to zoom:
That’s the whole thing — one element, one src.
The markup
Stripped to essentials, the element above is just:
<model-viewer
src="https://modelviewer.dev/shared-assets/models/NeilArmstrong.glb"
poster="https://modelviewer.dev/shared-assets/models/NeilArmstrong.webp"
environment-image="https://modelviewer.dev/shared-assets/environments/moon_1k.hdr"
alt="Neil Armstrong's Spacesuit"
camera-controls
auto-rotate
ar
shadow-intensity="1"
></model-viewer>camera-controlslets the user orbit, pan, and zoom.auto-rotatespins it slowly until the user interacts.aradds a “view in your space” button on phones that support it.shadow-intensityand a handful of lighting attributes tune how it’s lit.
Using it in React
Since it’s a custom element, “using it in React” is mostly just importing the package once so the browser registers the tag, then writing the element in your JSX:
import '@google/model-viewer';
export function AssetPreview({ src }) {
return (
<model-viewer
src={src}
alt="3D asset preview"
camera-controls
auto-rotate
ar
shadow-intensity="1"
style={{ width: '100%', height: '420px' }}
/>
);
}Two React-specific gotchas worth knowing:
- Import for the side effect.
import '@google/model-viewer'doesn’t give you a component — it registers the<model-viewer>element globally. You do it once, anywhere in your app. - Attributes vs. props. Web components communicate through attributes, so
camera-controlsand friends are written as plain attributes, and anything richer than a string is set imperatively via arefrather than passed as a prop.
Thumbnails
The other half of my upload component was generating a still image per model so the browse grid stayed fast. model-viewer has this built in — once a model is loaded you can ask the element for a rendered frame:
// `viewer` is the <model-viewer> element, after its 'load' event has fired.
const dataUrl = viewer.toDataURL('image/webp');The nice part is that the upload component was already rendering the model in a <model-viewer> to preview it before you hit submit — so the thumbnail was free. Right
there in the browser, at upload time, I grabbed the frame with toDataURL() and sent it
up alongside the .glb in the same request. No headless browser, no server-side
rendering step — the preview the user was already looking at became the thumbnail. The
grid then loaded plain <img> tiles and only spun up a real 3D viewer on the detail
page. Same library, two jobs: cheap static previews for the list, full interactivity for
the one asset you actually clicked into.
Why reach for it
If you need a full 3D scene — custom cameras, physics, post-processing — you want three.js (or react-three-fiber) directly. But when the job is “show this one model and let people spin it around,” model-viewer gets you there in a single element, handles the WebGL, loading states, and accessibility for you, and looks good doing it. For an asset-library app where every screen is really just “here’s a model,” it was exactly the right altitude.