3D models in React

@navalm 2019-07-01 tags: 3dreactfrontend views: —

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:

loading 3D model…

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-controls lets the user orbit, pan, and zoom.
  • auto-rotate spins it slowly until the user interacts.
  • ar adds a “view in your space” button on phones that support it.
  • shadow-intensity and 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-controls and friends are written as plain attributes, and anything richer than a string is set imperatively via a ref rather 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.