A focused, framework-independent 3D product viewer for the web.
CuViq provides a native <cuviq-viewer> Web Component for displaying GLB and
hosted glTF models. It handles loading, product lighting, camera fitting,
rotation, bounded zoom, responsive sizing, lazy activation, and cleanup without
requiring an iframe or a framework-specific wrapper.
npm package · API reference · model delivery guide · release notes
- What it does
- Quick start
- Loading models
- How it works
- Interaction
- API
- Framework integrations
- Sizing and styling
- Accessibility
- Performance and model preparation
- CORS and security
- Browser support
- Limitations
- Troubleshooting
- Development
- License
CuViq is designed for one job: letting someone inspect a product model in a clean, responsive viewport. It provides:
- URL loading for binary
.glbfiles and hosted.gltffiles with external buffers and textures. - Programmatic loading of an HTTP(S) URL or a local single-file GLB through
load(). - Automatic centering and camera fitting based on the model's geometry.
- Mouse, trackpad, touch rotation, and model-relative bounded zoom.
- A responsive square layout by default, with optional rectangular dimensions.
- Lazy initialization, poster images, demand-driven rendering, and off-screen suspension.
- Stable ready/error events, source replacement, WebGL context recovery, and deterministic Three.js resource cleanup.
- An accessible host element with an alternative description and live loading or error status.
- Native use in plain HTML, React, Angular, Vue, or any environment that accepts Web Components.
CuViq intentionally has no toolbar, pan mode, auto-rotation, analytics, fullscreen UI, AR, hotspots, or product configurator. Applications remain in control of their own interface.
npm install cuviq-viewerImport the auto-registration entry once in browser code:
import "cuviq-viewer/auto";Then use the element in markup:
<cuviq-viewer
src="/models/product.glb"
poster="/images/product.webp"
alt="Interactive 3D view of the product"
loading="eager"
></cuviq-viewer>The model path is resolved against the document base URL. Put the file in a
directory served by your application—for example, public/models in a Vite
application. Models must be loaded over HTTP(S); double-clicking an HTML file
and opening it with a file: URL is not supported. A page-level <base>
element changes how relative model URLs resolve.
The npm tarball includes a small brass sample at
node_modules/cuviq-viewer/examples/models/brass-ferrule-block.glb. Copy it
into your application's public/static assets before referencing it from a page;
it is a sample file, not a JavaScript export.
The standalone ESM bundle includes Three.js and registers the element automatically:
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>CuViq example</title>
<style>
cuviq-viewer {
width: min(100%, 640px);
aspect-ratio: 4 / 3;
}
</style>
<script
type="module"
src="https://unpkg.com/cuviq-viewer@0.1.0/dist/browser/cuviq.js"
></script>
</head>
<body>
<cuviq-viewer
src="/models/product.glb"
alt="Interactive 3D view of the product"
></cuviq-viewer>
</body>
</html>Pin a package version in production, as shown above. Alternatively, copy
dist/browser/cuviq.js from the installed package and serve it from your own
origin.
| Source | How to load it | Notes |
|---|---|---|
| Hosted GLB | src or load(url) |
Preferred: one binary file containing the model and its resources. |
| Hosted glTF | src or load(url) |
External buffers and textures resolve relative to the .gltf URL. |
Local GLB File |
load(file) |
The filename must end in .glb and use a supported binary MIME type. |
Local GLB Blob |
load(blob) |
Use a supported binary MIME type. |
String sources may be absolute HTTP(S) URLs or relative URLs. data:, blob:,
and file: string URLs are rejected; use load(File) or load(Blob) for local
content. For both File and Blob, supported MIME types are
model/gltf-binary, application/octet-stream, or an empty value. Local
multi-file glTF is not supported.
<cuviq-viewer
src="https://cdn.example.com/products/chair.glb"
alt="Walnut dining chair"
></cuviq-viewer>You can also change src at any time. A newer source supersedes an older load,
so a slow earlier request cannot replace the most recently requested model.
const viewer = document.querySelector("cuviq-viewer");
viewer.src = "/models/product-blue.glb";This example assumes the HTML is processed by Vite or another bundler that resolves npm package imports:
<input id="model-file" type="file" accept=".glb,model/gltf-binary" />
<cuviq-viewer id="viewer" alt="Selected local 3D model"></cuviq-viewer>
<script type="module">
import "cuviq-viewer/auto";
const input = document.querySelector("#model-file");
const viewer = document.querySelector("#viewer");
input.addEventListener("change", async () => {
const file = input.files?.[0];
if (!file) return;
try {
await viewer.load(file);
} catch (error) {
console.error("Could not display the model", error);
}
});
</script>load() resolves after the first successful frame. On failure it rejects and
also dispatches cuviq-error.
cuviq-viewer/autodefines one idempotent native custom element. Each element instance owns an open Shadow DOM containing its poster and status. When the viewer activates, it adds its WebGL canvas.- With the default
loading="lazy", anIntersectionObserverwaits until the viewer is near the viewport.loading="eager"starts immediately. - Three.js
GLTFLoaderloads the GLB or hosted glTF and resolves any hosted dependencies relative to the glTF document. - CuViq calculates the model bounds, centers the orbit target, fits a perspective camera, and derives safe near, far, and zoom distances from the model size.
- A generated room environment, sRGB output, and ACES filmic tone mapping provide neutral product lighting without downloading a hidden HDR image.
- Orbit controls expose rotation and zoom only. Rendering runs while a load, interaction, resize, or settling animation needs frames, then stops.
- Rendering suspends while the viewer or document is hidden. Replaced models and disconnected viewers dispose their geometry, materials, textures, controls, renderer, and observers.
If the WebGL context is lost, CuViq reports the condition. When the context is restored, it reloads the current source.
| Input | Result |
|---|---|
| Mouse drag | Rotate the model. |
| Mouse wheel or trackpad | Zoom within model-relative limits. |
| One-finger horizontal drag | Rotate the model. |
| One-finger vertical gesture | Continue scrolling the page. |
| Two-finger gesture | Dolly/zoom and rotate. |
Horizontal orbit is continuous and vertical orbit is constrained to avoid flipping over the poles. Panning is disabled, so the product remains centered.
| Name | Type | Default | Description |
|---|---|---|---|
src |
string |
none | HTTP(S) or relative GLB/hosted glTF URL. Empty removes the current model. |
poster |
string |
none | Image displayed before the first ready frame. |
loading |
"lazy" | "eager" |
"lazy" |
Load near the viewport or initialize immediately. |
alt |
string |
none | Accessible model description and fallback accessible name. |
aria-label |
string |
none | Explicit accessible name; takes precedence over alt. |
width |
positive number | responsive | Host width in CSS pixels. The property also accepts null to remove it. |
height |
positive number | automatic | Host height in CSS pixels. The property also accepts null to remove it. |
src, poster, loading, alt, width, and height have matching element
properties. Invalid numeric values in markup are ignored; assigning a non-finite
or non-positive width or height property throws RangeError.
type CuviqSource = string | File | Blob;
viewer.load(source: CuviqSource): Promise<void>;Use this method for local GLB files, awaited loading, or programmatic URL loads.
Both events bubble and cross the Shadow DOM boundary.
| Event | Detail | When it fires |
|---|---|---|
cuviq-ready |
{ source: string | File | Blob } |
After the first successful frame. |
cuviq-error |
{ code, message, source? } |
When a stable public error is available. |
const viewer = document.querySelector("cuviq-viewer");
viewer.addEventListener("cuviq-ready", (event) => {
console.log("Loaded", event.detail.source);
});
viewer.addEventListener("cuviq-error", (event) => {
console.error(event.detail.code, event.detail.message);
});Error codes are:
| Code | Meaning |
|---|---|
UNSUPPORTED_SOURCE |
The URL, local file, or MIME type is unsupported. |
LOAD_FAILED |
The source could not be fetched or parsed. |
RESOURCE_MISSING |
A model or one of its referenced resources is missing. |
CORS_ERROR |
A network or browser cross-origin policy prevented access. |
DECODER_REQUIRED |
The model needs a decoder not included in CuViq V1. |
MODEL_EMPTY |
The model has no usable finite geometry bounds. |
WEBGL_UNAVAILABLE |
WebGL could not be initialized. |
WEBGL_CONTEXT_LOST |
The active graphics context was lost. |
Assign a valid new src or call load() again to recover from a model-loading
error. See API.md for the complete TypeScript contract.
| Import | Purpose |
|---|---|
cuviq-viewer |
Class, constants, types, and explicit registration. SSR-safe and does not auto-register. |
cuviq-viewer/auto |
Registers <cuviq-viewer> as an import side effect. Use this in most bundled browser applications. |
cuviq-viewer/react |
Optional React 19 scoped JSX declarations. This entry is type-only. |
cuviq-viewer/browser |
Standalone ESM browser bundle with Three.js included. |
For explicit registration:
import { defineCuviqViewer } from "cuviq-viewer";
defineCuviqViewer();Repeated registration is safe. The package is ESM-only.
CuViq is the same browser-native element in every framework. It does not add React, Angular, or Vue as runtime dependencies.
Import the optional type entry so React's scoped JSX namespace recognizes the element. A ref plus native event listeners provides typed custom events and safe cleanup under Strict Mode.
import { useEffect, useRef } from "react";
import "cuviq-viewer/auto";
import type {} from "cuviq-viewer/react";
import type { CuviqReadyDetail, CuviqViewerElement } from "cuviq-viewer";
export function ProductModel() {
const viewer = useRef<CuviqViewerElement>(null);
useEffect(() => {
const element = viewer.current;
if (!element) return;
const onReady = (event: Event) => {
const { source } = (event as CustomEvent<CuviqReadyDetail>).detail;
console.log("Loaded", source);
};
element.addEventListener("cuviq-ready", onReady);
return () => element.removeEventListener("cuviq-ready", onReady);
}, []);
return (
<cuviq-viewer
ref={viewer}
src="/models/product.glb"
poster="/images/product.webp"
alt="Interactive 3D product model"
width={640}
height={480}
/>
);
}Complete sample: examples/react/ProductModel.tsx.
In an SSR framework such as Next.js, import cuviq-viewer/auto from a client
module (for example, a module beginning with "use client"). The root
cuviq-viewer entry itself is safe to import on the server.
Add CUSTOM_ELEMENTS_SCHEMA to the component or NgModule that uses the custom
element:
import { Component, CUSTOM_ELEMENTS_SCHEMA } from "@angular/core";
import "cuviq-viewer/auto";
@Component({
selector: "app-product-model",
standalone: true,
schemas: [CUSTOM_ELEMENTS_SCHEMA],
template: `
<cuviq-viewer
src="/models/product.glb"
alt="Interactive 3D product model"
[width]="640"
[height]="480"
(cuviq-ready)="onReady($event)"
(cuviq-error)="onError($event)"
></cuviq-viewer>
`,
})
export class ProductModelComponent {
onReady(event: Event): void {
console.log((event as CustomEvent).detail);
}
onError(event: Event): void {
console.error((event as CustomEvent).detail);
}
}Complete sample:
examples/angular/product-model.component.ts.
Tell Vue's template compiler that cuviq-viewer is a custom element:
// vite.config.ts
import { defineConfig } from "vite";
import vue from "@vitejs/plugin-vue";
export default defineConfig({
plugins: [
vue({
template: {
compilerOptions: {
isCustomElement: (tag) => tag === "cuviq-viewer",
},
},
}),
],
});Then use it normally:
<script setup lang="ts">
import "cuviq-viewer/auto";
function onReady(event: Event): void {
console.log((event as CustomEvent).detail);
}
</script>
<template>
<cuviq-viewer
src="/models/product.glb"
alt="Interactive 3D product model"
:width="640"
:height="480"
@cuviq-ready="onReady"
/>
</template>Complete samples: examples/vue/ProductModel.vue
and examples/vue/vite.config.ts.
Without dimension attributes, the host uses the available width and a square
1 / 1 aspect ratio. Common configurations are:
| Dimensions | Result |
|---|---|
| Neither supplied | Responsive square: width: 100%. |
width only |
Fixed-width square. |
height only |
Available width with a fixed height. |
width and height |
Explicit rectangular viewport. |
Normal page CSS can override host dimensions and is usually preferable for a responsive layout:
.product-viewer {
width: min(100%, 42rem);
aspect-ratio: 4 / 3;
}<cuviq-viewer
class="product-viewer"
src="/models/product.glb"
alt="Interactive 3D product model"
></cuviq-viewer>The visual shell exposes three CSS custom properties:
cuviq-viewer {
--cuviq-bg: #f4f6f7;
--cuviq-radius: 18px;
--cuviq-border-color: #d8dee2;
}The internal scene, status, and canvas are isolated in Shadow DOM. V1 does not expose Shadow Parts.
- The connected host defaults to
role="img". altsupplies the accessible name when no explicitaria-labelis present.aria-labeltakes precedence overalt.- The internal WebGL canvas is hidden from assistive technology and removed from keyboard focus.
- Loading status is polite; error status uses an assertive live region.
- Reduced-motion preferences disable the poster fade and loading animation.
- A vertical one-finger gesture can scroll the containing page.
CuViq intentionally provides no keyboard camera controls. Always supply a useful
alt or aria-label, and provide essential product information outside the 3D
canvas as normal text.
A single GLB is the simplest and most reliable delivery format. Recommended budgets are:
| Asset property | Recommendation | High-risk threshold |
|---|---|---|
| Transfer size | 3–8 MB | Above 20 MB |
| Triangles | At most 300,000 | Above 800,000 |
| Individual texture | At most 2048 × 2048 | 4096 × 4096 without demonstrated benefit |
Additional guidance:
- Keep transforms finite and ensure the model contains visible geometry.
- Use
loading="lazy"for below-the-fold viewers andloading="eager"for the primary immediately visible product. - Prefer one active visible viewer when possible. Each activated instance owns a WebGL renderer and context.
- Device pixel ratio is capped to limit GPU cost: 1.5 on coarse-pointer devices and 2 on desktop-class pointers.
- Use long-lived immutable caching for versioned assets and normal HTTP compression where it helps.
See MODEL_DELIVERY.md for MIME types, caching, CORS, and asset validation guidance.
For a cross-origin model, the server must return an Access-Control-Allow-Origin
header that permits the page origin. A hosted glTF needs valid CORS headers not
only for the .gltf document, but also for every referenced buffer and texture.
Preserve their relative paths when deploying them.
For Content Security Policy deployments, account for:
- CuViq's script origin in
script-src. - Model, buffer, and texture origins in
connect-src. - Poster and texture origins in
img-srcfor browser image-loading paths. - The component's Shadow DOM
<style>element under a strictstyle-srcpolicy; test the exact policy in each target browser.
This is integration guidance rather than an exhaustive CSP. Test the final policy with representative embedded and external textures. CuViq V1 creates no workers and makes no background requests beyond the requested model and its declared dependencies—there are no analytics, telemetry, decoder, or HDR downloads.
CuViq targets current Chrome, Edge, Safari, iOS Safari, and Android Chrome on devices with WebGL 2. Automated Chromium tests cover model loading, local files, lazy activation, source races, recovery, dimensions, interaction smoke tests, idle rendering, multiple instances, and simulated WebGL context events.
Physical iPhone/Android and desktop Edge/Safari acceptance is still tracked as a release gate; desktop emulation is not treated as physical mobile validation. See docs/MANUAL_DEVICE_QA.md and docs/ACCEPTANCE.md for the current evidence.
CuViq V1 deliberately does not include:
- Draco, KTX2/Basis, or Meshopt decoder payloads. Models requiring them report
DECODER_REQUIRED. - Local multi-file glTF loading.
- Animation playback or authored-camera selection.
- Panning, auto-rotation, a reset button, or a visible toolbar.
- Fullscreen, screenshots, hotspots, measurements, annotations, or AR.
- Ground planes, shadows, post-processing, or downloadable HDR environments.
- Progress, camera-change, screenshot, fullscreen, or analytics events.
| Symptom | What to check |
|---|---|
| The element is unknown in React TypeScript | Add import type {} from "cuviq-viewer/react" in React 19 code. |
| The element never registers | Import cuviq-viewer/auto once from client/browser code, or call defineCuviqViewer(). |
The viewer stays in waiting |
It is using lazy loading and is not near the viewport. Try loading="eager" and confirm it has visible dimensions. |
| A model URL works locally but not after deployment | Check the deployed path, response status, MIME type, base URL, and CORS headers in browser developer tools. |
Hosted glTF reports RESOURCE_MISSING |
Check every referenced .bin and texture URL, including filename case and CORS. |
A model reports DECODER_REQUIRED |
Re-export it without Draco, Meshopt, or KTX2/Basis compression. |
A local model reports UNSUPPORTED_SOURCE |
Use a single .glb file with a supported binary MIME type and call load(file). |
| The packaged sample returns 404 | Copy it from node_modules/cuviq-viewer/examples/models into your app's public/static directory. |
| The page was opened directly from disk | Serve it from a local HTTP development server; string sources accept only HTTP(S). |
Listen for cuviq-error and log its code, message, and source while
diagnosing a failed load.
Requirements: Node.js 18 or newer and a browser with WebGL 2.
git clone https://github.com/github4me/CuViq.git
cd CuViq
npm ci
npx playwright install chromium
npm run checkUseful commands:
| Command | Purpose |
|---|---|
npm run preview |
Build the package and start the local Vite preview. |
npm run typecheck |
Check the TypeScript sources and integration typings. |
npm test |
Run the unit test suite. |
npm run test:e2e |
Generate fixtures and run browser E2E tests. |
npm run build |
Build library ESM, standalone browser ESM, source maps, and declarations. |
npm run check |
Run type checking, unit tests, all builds, bundle budgets, SSR validation, and E2E tests. |
After npm run preview, use:
/preview.htmlfor the locally built bundle preview./preview-npm.htmlfor the published standalone npm bundle./preview-react.htmlfor the React integration preview.
- API reference
- Model delivery guide
- Release notes
- Implementation decisions
- Acceptance evidence
- Manual device QA
- Third-party notices
Issues and focused pull requests are welcome at the GitHub repository.
CuViq is available under the MIT License. Three.js attribution is included in THIRD_PARTY_NOTICES.md.