AETumi — AI-native 3D web platform for Three.js, WebGL and interactive websites
News & Guides

Three.js Product Configurator: The Production Build Guide

September 8, 2026 · AETumi

Key answer / TL;DR: A Three.js product configurator is an interactive 3D viewer that lets a shopper build their own version of a product — swapping colors, materials, and parts and seeing the result update live, usually with the price. It works by keeping a single plain configuration object (the shopper's choices) separate from the render state (the Three.js scene), and applying that config to the loaded model: set material colors, swap textures, and toggle part visibility, then render once. A production Three.js product configurator needs four things a demo skips: serializable config that maps to a real SKU and price, on-demand rendering so the GPU idles between changes, a shareable URL that encodes the configuration, and real HTML controls so the page stays indexable and accessible. Done right, it turns "which options exist?" into "here's exactly what I'm buying," which is why configurable products — furniture, footwear, watches, cars, hardware — convert on it. AETumi ships this as production 3D components you adapt via the AETumi MCP instead of wiring materials to a blank scene. This guide is the full build in correct Three.js r160 code, with honest trade-offs — including when a variant image grid is the better call.

Table of contents

What you'll learn

  • What separates a production three.js product configurator from a demo that swaps a color and calls it done.
  • How to model the shopper's choices as one serializable config object that maps to a real SKU and price.
  • Correct Three.js r160 code to swap colors and materials, toggle parts, and render on demand.
  • How to encode the configuration in a shareable URL so a shopper can send their build to someone.
  • A performance pipeline: compressed GLB, capped pixel ratio, idle-when-static rendering, and disposal.
  • How to keep the configurator indexable and accessible with real HTML controls and structured data.
  • A decision matrix for choosing between a 3D configurator, a variant image grid, and a rendered video.

What is a product configurator

A product configurator is an interactive tool that lets a customer assemble their own version of a product from a defined set of options — color, material, finish, size, components — and see the result immediately. A 3D configurator does this in a real-time rendered scene: the shopper picks "walnut frame, forest-green wool, brass legs," and the model on screen updates to that exact combination, from any angle, with the price adjusting alongside.

From idea to productionAETumi technical diagram — From idea to production01Start from owned source02Adapt props & variants03Wire server/client boundary04Handle loading/empty/error05Ship & reuse
From idea to production
Halcyon
Halcyon — live preview from the AETumi library

Under the hood it is a mapping problem. The shopper's choices form a small configuration object — plain data like { frame: 'walnut', fabric: 'forest', legs: 'brass' }. A thin adapter reads that object and applies it to the loaded Three.js model: setting a material's color, swapping a texture, or toggling a part's visibility. The scene never stores the choices; it just reflects them. That separation — config as data, scene as a view of the data — is what keeps a product configurator maintainable as the option list grows from three choices to thirty.

Why it matters for configurable products

Some products are sold because they can be personalized. Furniture in a dozen fabrics, sneakers with colorable panels, watches with interchangeable straps and cases, cars, kitchens, eyewear — the value is that the customer gets their version, not a fixed one. A grid of pre-rendered variant photos can show a handful of combinations, but the moment the option count multiplies, you can't photograph every permutation. A configurator generates the shopper's exact combination on demand, which is the only practical way to show 3 frames × 8 fabrics × 4 legs = 96 variants without shooting 96 photos.

Epoxy Drift
Epoxy Drift — live preview from the AETumi library

That value is real but not free. A configurator needs a well-authored model with separable, correctly-named parts and materials; it adds engineering over a plain viewer; and like all WebGL it carries load and battery cost on low-end mobile. The honest framing is the same as any 3D enhancement: it earns its weight when personalization drives the sale and the combination count is too large to photograph, and it is dead weight when the product ships in three colors you can simply show as images. The decision matrix below routes each case.

Core capabilitiesAETumi technical diagram — Core capabilitiesTypedsourceARIA /keyboardDesigntokensServer/ClientLoading/empty/error
Core capabilities

Architecture: config vs render state

The single most useful decision in the build is to separate configuration from render state, the same discipline a good product viewer uses to separate product state from render state. Configuration is authored, serializable data: the option groups, the allowed values, and the shopper's current selection. Render state is Three.js internals — the camera, the loop, the loaded scene graph, the materials currently applied.

Jungle Canopy
Jungle Canopy — live preview from the AETumi library

When these are tangled, each option's effect is hardcoded into a click handler, and adding a finish means editing imperative render code. When they're separate, the UI only ever writes to the config object, and one adapter applies the whole config to the scene. The generator's architecture diagram above maps this: option data and the current selection on the left, an apply-config adapter in the middle, the Three.js scene on the right, with the price and the shareable URL both derived from the same config. Derive everything — the render, the price, the URL, the SKU — from one object, and the configurator stays coherent no matter how many options you add.

// Configuration is plain, serializable data — the single source of truth.
const options = {
  frame:  { walnut: 0x5a3a22, oak: 0xb98a4b, black: 0x1a1a1a },
  fabric: { forest: '/tex/forest.webp', sand: '/tex/sand.webp', navy: '/tex/navy.webp' },
  legs:   { brass: 0x8a6a3b, steel: 0x9aa0a6, black: 0x1a1a1a },
};
const config = { frame: 'walnut', fabric: 'forest', legs: 'brass' }; // shopper's choices

Expected behavior: the entire state of the configurator is captured in one small object you can serialize, log, and map to a SKU — the scene is just a view of it. Trade-off: this indirection is slightly more code than hardcoding a few click handlers, and for a genuinely fixed three-color product it's overkill — but the moment options multiply, config-as-data is what stops the render loop from becoming spaghetti.

Expert Note — Make the config the only thing your UI writes to, and derive the render, the price, and the URL from it — never the reverse. When a shopper clicks "navy," update config.fabric = 'navy' and let a single applyConfig reflect it; don't reach into the scene from the click handler. This one rule keeps the price, the 3D view, and the shareable link perfectly in sync by construction, because they all read from the same source instead of being updated in three places you can forget.

Apply the whole config in one pass

With config modeled, the core of the configurator is a single function that reads it and updates the scene. Everything else — clicks, sliders, URL loading — just changes the config and calls this.

Rift Stone
Rift Stone — live preview from the AETumi library
import * as THREE from 'three';

const texLoader = new THREE.TextureLoader();
const texCache = new Map(); // avoid reloading textures on repeat selections

function applyConfig(model, cfg) {
  model.traverse((o) => {
    if (!o.isMesh || !o.material) return;
    if (o.name === 'Frame') o.material.color.setHex(options.frame[cfg.frame]);
    if (o.name === 'Legs')  o.material.color.setHex(options.legs[cfg.legs]);
    if (o.name === 'Seat') {
      const url = options.fabric[cfg.fabric];
      let tex = texCache.get(url);
      if (!tex) { tex = texLoader.load(url); tex.colorSpace = THREE.SRGBColorSpace; texCache.set(url, tex); }
      o.material.map = tex;
      o.material.needsUpdate = true; // required after swapping a texture map
    }
  });
}

Expected behavior: calling applyConfig(model, config) updates every affected mesh to match the current selection in one pass, using cached textures on repeat picks. Trade-off: this relies on the model's parts being named correctly (Frame, Legs, Seat) in the GLB — naming is authoring work you do in your DCC tool, and a mislabeled mesh silently won't update. Establish a naming convention with whoever authors the models and validate it on load.

Swap colors and materials cleanly

Color swaps are the cheapest change — just set material.color. Material swaps (matte vs gloss, fabric vs leather) are heavier because they change how light responds, so change properties on the existing material rather than allocating a new one each time.

// Change finish without allocating a new material every click.
function setFinish(mesh, finish) {
  const m = mesh.material; // MeshStandardMaterial
  if (finish === 'matte')  { m.roughness = 0.9; m.metalness = 0.0; }
  if (finish === 'satin')  { m.roughness = 0.5; m.metalness = 0.1; }
  if (finish === 'gloss')  { m.roughness = 0.1; m.metalness = 0.2; }
  m.needsUpdate = true;
}

Expected behavior: the same mesh shifts from matte to gloss by adjusting roughness and metalness, with no new material allocation and no GPU churn. Trade-off: mutating a shared material affects every mesh that references it — if two parts share one material and should differ, clone the material once at load (mesh.material = mesh.material.clone()) so each part is independent. Do that cloning up front, not on every click, to avoid leaking materials.

Toggle parts and optional components

Configurators often add or remove components — armrests, a roof rack, a strap, packaging. Model each optional part as its own mesh or group and toggle visibility from the config.

function applyParts(model, cfg) {
  const armrests = model.getObjectByName('Armrests');
  if (armrests) armrests.visible = !!cfg.armrests; // show/hide from config
  const headrest = model.getObjectByName('Headrest');
  if (headrest) headrest.visible = !!cfg.headrest;
}

Expected behavior: toggling config.armrests shows or hides that component instantly, and because visibility is derived from config, the shareable URL and price stay consistent with what's on screen. Trade-off: hidden meshes still cost memory (they're loaded, just not drawn), so for very large optional assemblies consider loading them on demand rather than shipping every option in one GLB. For a handful of small parts, visibility toggling is simpler and fast enough.

Live pricing and SKU mapping

A configurator that doesn't show the price is a toy. Derive the price from the same config object, and map the config to a real SKU so the cart knows what was built.

const basePrice = 1290;
const priceDelta = {
  fabric: { forest: 0, sand: 0, navy: 40 },
  legs:   { brass: 120, steel: 0, black: 0 },
  armrests: { true: 90, false: 0 },
};
function priceFor(cfg) {
  return basePrice
    + priceDelta.fabric[cfg.fabric]
    + priceDelta.legs[cfg.legs]
    + priceDelta.armrests[String(!!cfg.armrests)];
}
function skuFor(cfg) {
  return `CHAIR-${cfg.frame}-${cfg.fabric}-${cfg.legs}${cfg.armrests ? '-ARM' : ''}`.toUpperCase();
}

Expected behavior: every configuration yields a deterministic price and a SKU string the cart and backend can act on, both derived from the one config object. Trade-off: the price and SKU logic must stay authoritative on the server too — never trust a price computed in the browser at checkout. Treat the client-side price as display, and recompute and validate the SKU and price server-side when the order is placed.

Shareable configuration URLs

Personalization is social — shoppers want to send their build to a partner or save it. Encode the config in the URL so any configuration is a shareable, bookmarkable link.

// Serialize config to the URL, and restore it on load.
function writeUrl(cfg) {
  const q = new URLSearchParams(cfg).toString();
  history.replaceState(null, '', `?${q}`); // no navigation, just update the address
}
function readUrl() {
  const q = new URLSearchParams(location.search);
  return {
    frame: q.get('frame') || 'walnut',
    fabric: q.get('fabric') || 'forest',
    legs: q.get('legs') || 'brass',
    armrests: q.get('armrests') === 'true',
  };
}

Expected behavior: as the shopper configures, the URL updates without a page reload, and opening a shared link restores the exact build. Trade-off: URL params are visible and user-editable, so validate every value against your options map on load — never apply a param you didn't offer, or a crafted link could request a material that doesn't exist. Keep the URL to real option keys, not sensitive data, and let the server be the authority on what's valid.

Real product evidence

The demo below is a real AETumi 3D component running the exact configurator architecture in this guide. Watch what it proves: choices update the model live from a single config object, the price and view stay in lockstep because both read from that object, and the loop idles the instant you stop changing options — so a settled configuration costs zero frames. That state discipline is the difference between a configurator that scales to dozens of options and one that drifts out of sync.

What the clip doesn't show is just as instructive: no layout shift as the model loads behind its poster, and no GPU churn between selections because textures are cached and materials are mutated rather than reallocated. That restraint is what makes a 3d product customizer shippable on real devices instead of a demo that stutters after the tenth click.

Performance architecture

A configurator invites lots of interaction, so performance is architecture, not polish. The practical rules:

  • Ship one compressed GLB with all base parts: Draco for geometry, KTX2 (Basis) for textures, both loaded from three/addons.
  • Cache textures so re-selecting a finish never re-downloads it, as shown above.
  • Mutate, don't allocate. Change color, roughness, and metalness on existing materials; clone once at load only where parts must differ.
  • Render on demand. Draw after a config change or an orbit, then idle — a settled configuration should cost nothing.
  • Dispose on unmount. Release geometries, materials, textures, controls, and the renderer, or a single-page storefront leaks GPU memory until the tab crashes.
function disposeConfigurator(scene, renderer, controls, texCache) {
  scene.traverse((o) => {
    if (!o.isMesh) return;
    o.geometry?.dispose();
    (Array.isArray(o.material) ? o.material : [o.material]).forEach((m) => { m?.map?.dispose(); m?.dispose(); });
  });
  texCache.forEach((t) => t.dispose()); // release cached textures too
  controls?.dispose();
  renderer.dispose();
}

Expected behavior: after disposeConfigurator, GPU memory returns to baseline, including the texture cache the configurator built up. Trade-off: React Three Fiber disposes most scene resources on unmount, but your texture cache is app-level state R3F doesn't know about — you still dispose that yourself. Forgetting the cache is the subtle leak specific to configurators over plain viewers.

Expert Note — Preload the textures for the most popular options, not all of them. If nine in ten shoppers pick one of three fabrics, warm those into the cache after first paint and lazy-load the long tail on selection. This keeps the initial payload small while making the common path feel instant — the opposite of shipping every texture up front and paying for combinations most shoppers never choose. Measure which options are actually picked and preload to the data, not to the full option list.

When to build a configurator

Build a Three.js configurator when…Why it pays off
The product is genuinely personalizableThe shopper buys their version, not a fixed one
Combinations are too many to photograph96 variants beat 96 photo shoots
Personalization drives the purchase decisionSeeing the exact build removes the last doubt
You have a clean model with named parts/materialsConfig maps cleanly to meshes
It's a considered, high-value purchaseEngagement cost is justified by order value

When NOT to

Avoid a configurator when…Use instead
The product ships in a few fixed variantsA variant image grid or swatches
Options don't change what the customer seesA simple dropdown, no 3D
You lack a clean, correctly-named modelPhotos — don't fake configuration
The audience is heavily low-end mobileVariant images + optional single 3D view
The catalog is huge and each SKU is simpleStatic pages with photographed variants
Personalization doesn't affect the decisionA plain product page with good photography

The rule: a configurator earns its complexity when personalization is the reason to buy and the combination count defeats photography. If either isn't true, a variant grid is faster, cheaper, and converts just as well.

Configurator vs variant grid vs video

Match the medium to how the product is chosen, not to the trend.

Factor3D configuratorVariant image gridRendered / shot video
Shows any combinationYes, generated on demandOnly photographed onesOnly filmed ones
Shopper controlFull — build and orbitPick from a fixed setNone (linear playback)
Combination scalingExcellent (data-driven)Poor (one photo each)Poor
Page weightHigh (GLB + runtime)Low–medium (images)Medium
Live price feedbackYes, from configPossible per variantNo
Shows a working mechanismLimitedNoYes
Per-product costHigh (model + materials)Medium (photography)Medium
Best forMany-option personalized productsFew fixed variantsProducts defined by motion/use

Read it as routing: many options and personalization drives the sale → configurator; a handful of fixed variants → an image grid with swatches; the product is about motion or use → video. Strong pages often pair a static hero image (for load and SEO) with the configurator behind it, exactly the hybrid the static vs 3D trade-off recommends.

SEO and accessibility

A <canvas> is invisible to crawlers and screen readers, so the product facts, the option list, and the price must live in real HTML, and the controls must be real form controls — not clickable <div>s. The 3D view enhances accessible controls; it never replaces them.

<form id="configurator" aria-label="Configure your Larsen Lounge Chair">
  <fieldset>
    <legend>Fabric</legend>
    <label><input type="radio" name="fabric" value="forest" checked> Forest wool</label>
    <label><input type="radio" name="fabric" value="sand"> Sand wool</label>
    <label><input type="radio" name="fabric" value="navy"> Navy wool (+$40)</label>
  </fieldset>
  <p class="price" aria-live="polite">$1,290</p>
  <canvas id="scene" aria-label="3D preview of the configured chair"></canvas>
  <button type="submit">Add configured chair to cart</button>
</form>

Expected behavior: the configurator is fully operable by keyboard and screen reader through real radio inputs, the price is announced on change via aria-live, and the page's options and price are crawlable HTML with WebGL disabled. Trade-off: you maintain the option list in HTML and the config map in code — mild duplication — but it's the only way the configurator stays indexable and accessible. Point og:image at a rendered still of a representative build, respect prefers-reduced-motion by not auto-rotating, and keep Product structured data on the page. For the base viewer this sits on, see the Three.js product viewer guide.

How AETumi approaches it

AETumi is an AI-native 3D web platform: a library of production Three.js and WebGL components, 3D scenes, and React and Next.js parts, plus the AETumi MCP for AI coding assistants like Claude Code, Cursor, and Codex. Rather than wiring materials and part toggles onto a blank scene, you install a configurator component that already implements the architecture here — config-as-data, one-pass apply, texture caching, on-demand rendering, shareable URLs, and disposal — and adapt it to your model and option map.

Because the platform exposes a Model Context Protocol server, an agent can pull the configurator and its dependencies straight into your project, then wire your part names, materials, and price deltas to your GLB. The components ship in vanilla and React Three Fiber form, so the customizer stays consistent with your stack, and the code is reviewable — you start from working state discipline instead of debugging a scene that drifts out of sync with the price. Browse the 3D components on aetumi.app to see the configurator this guide is built around.

Technical proof: the GitHub repo

The reference implementation lives at AETumiApp/threejs-product-viewer. It's a minimal, runnable Three.js r160 ES-module viewer whose architecture is exactly what a configurator extends: a capped-pixel-ratio renderer, a GLTFLoader + DRACOLoader pipeline for compressed GLB, damped OrbitControls, a poster fallback, render-on-demand, and disposal on unmount. The configurator logic here — config-as-data, one-pass applyConfig, material and part swaps, live price, and URL serialization — layers directly on top, because the render loop and state discipline are shared.

Be clear about its limits: the repo is a reference for the architecture, not a finished configurable storefront. It doesn't ship an authored multi-material model with named parts and a price map — that's product work against your own GLB — and it has no cart, no server-side price validation, and no KTX2 environment lighting; those are noted as extensions. It caps DPR and renders on demand, but your numbers depend on part count and texture budget, so measure with your own model on a real mid-range phone. Read it to understand the wiring, then build your option map on top.

How the pieces connectAETumi technical diagram — How the pieces connectDesignTokensPrimitiveVariantsStatesShip
How the pieces connect

FAQ

What is a Three.js product configurator? It's an interactive 3D tool, built with Three.js, that lets a shopper assemble their own version of a product — colors, materials, parts — and see it update live with the price. Technically, the shopper's choices form one plain config object, and a single adapter applies that object to the loaded model by setting material colors, swapping textures, and toggling part visibility. A production build also needs a SKU mapping, a shareable URL, on-demand rendering, and real HTML controls so the page stays indexable and accessible.

Do I need a special 3D model for a configurator? Yes — the model must have separable, correctly-named parts and materials. If the GLB is a single merged mesh with one material, there's nothing to configure. Author each configurable component as its own mesh with a clear name (Frame, Seat, Legs) so your apply-config function can target it, and clone shared materials at load where two parts must differ. Naming and separation are authoring work in your DCC tool, and a mislabeled mesh silently won't respond to the configurator.

How do I keep the price and the 3D view in sync? Derive both from one config object and never update them independently. When a shopper picks an option, write it to the config, then let one applyConfig update the scene and one priceFor compute the price from the same object. Because the render, the price, the SKU, and the shareable URL all read from a single source, they can't drift apart. Always recompute and validate the price server-side at checkout — the browser price is display only, never the authority.

Is a configurator bad for SEO? Not if you build it on real HTML. The canvas is invisible to crawlers, so the option list must be real form controls, and the product name, price, and Product structured data must be in the DOM. Done that way, the page ranks on its content whether or not WebGL loads, and the configurator is progressive enhancement. Add a rendered still of a representative build as og:image so social and image search have something concrete. A configurator that replaces HTML with canvas pixels will hurt SEO; one layered over accessible controls won't.

When is a variant image grid better than a configurator? When the product ships in a small, fixed set of variants — say three colors — a grid of photographs or swatches is faster to build, lighter to load, and converts just as well, with none of the modeling cost. The configurator earns its complexity only when the combination count is too large to photograph and personalization is the reason people buy. For a few fixed options, or a huge catalog of simple SKUs, reach for the image grid and skip the WebGL.

Conclusion

A Three.js product configurator is a state-management discipline, not a demo: one serializable config object as the source of truth, a single pass that applies it to a model with named parts, live pricing and a SKU derived from that same object, a shareable URL, on-demand rendering, honest disposal, and real HTML controls outside the canvas. Build it when personalization drives the sale and the combinations defeat photography — and reach for a variant grid or a video when they don't. You can assemble every piece from this guide, and the AETumiApp/threejs-product-viewer repo shows the shared wiring end to end. To start from working state discipline instead of a blank scene, pull AETumi's 3D components via the AETumi MCP — Full Stack ($129, buy once, own for life) includes the full source and the MCP. See the plans at aetumi.app/pricing, then load your model and map your options.

More from the AETumi library

Real, production-ready assets — preview the motion, grab the source.

Browse all 3D components →