Model viewer

Interactive 3D model viewer — embed a glTF/GLB mesh that the user can orbit, zoom and (on capable devices) place in AR. Self-hosted viewer (no third-party SaaS), poster-first lazy loading, reduced-motion + image fallback. No Material parity (M3 has no 3D viewer); Koder fills the gap for product catalogs, hardware showcases and Hub hero assets.

Facet Visual + Integration do Koder Design. No Material 3 equivalent — Koder addition. Built on the self-hosted <model-viewer> web component (glTF) + native quick-look on mobile.

KoderModelViewer embeds a real 3D mesh the user can orbit, zoom and inspect, with an optional AR "place in your space" mode. It is the only component that renders true geometry, so it is the on-ramp for the 3D / depth.kmd story on flat surfaces.

Use cases

  • Product catalog — papelaria e-commerce (Crescer / Vivver) item

    preview; rotate before buy.

  • Hardware / device showcase — Koder Kodix device, peripheral.
  • Hub hero asset — an app's 3D icon or mascot.
  • Spec/asset preview — inspect a 3D model bundled with a product.

R1 — Formats

Format Role
glTF / GLB Canonical. GLB (binary) preferred for single-file delivery. Draco / meshopt compression REQUIRED above 1 MB.
USDZ Companion file for iOS/visionOS AR Quick Look only. Generated from the GLB; never the source of truth.
Poster image MANDATORY. A static render (.webp / .png / .svg) shown before/instead of the live model (R3).

Self-hosted per self-hosted-first.kmd: the viewer runtime is bundled locally and served under script-src 'self'. No CDN, no model-hosting SaaS. Canonical web impl: <koder-model-viewer> (the koder-model-viewer.js wrapper) over the self-hosted @google/model-viewer engine vendored at /assets/vendor/model-viewer/. DRACO / meshopt decoders, if used, MUST also be self-hosted (they default to a Google CDN) — prefer uncompressed glTF for small assets to avoid that external decoder path.

R2 — Interaction

Gesture Action
Drag / arrow keys Orbit
Wheel / pinch Zoom (clamped min/max)
Two-finger / right-drag Pan (opt-in; off by default)
Auto-rotate Optional idle spin; off under prefers-reduced-motion

Auto-rotate, when on, spins slowly (≤ 30°/s, matching the XR motion- sickness ceiling, xr-preview.kmd § R8) and stops on interaction.

R3 — Loading & performance

Poster-first, always lazy:

  1. Render the poster image immediately (correct aspect, no layout

    shift — like cards.kmd § R5).

  2. Defer the viewer runtime + model until the element enters the

    viewport (IntersectionObserver) or the user activates the poster.

  3. Show a determinate progress indicator while the GLB downloads.

Budgets:

Budget Target
GLB transfer size ≤ 2 MB recommended; > 5 MB blocks release (warn)
First interactive < 1.5 s on mid-range mobile after activation
Frame rate 60 fps orbit; degrade mesh LOD before dropping frames
Wrapper JS (eager) ≤ 5 KB gzipped (koder-model-viewer.js) — safe on the critical path
Engine JS (lazy) @google/model-viewer ≈ 285 KB gzipped (three.js-based); MUST be lazy-loaded on poster activation / viewport entry, never on the critical path, loaded once + shared

R4 — Lighting, depth & theme

  • The scene's directional light reads the rig in

    lighting.kmd § R5 (azimuth/elevation) so a model and the cards around it are lit from the same angle.

  • Ground/contact shadow uses the umbra+penumbra pair (lighting.kmd § R2).
  • The viewer sits at a depth level like any surface

    (depth.kmd); its container shadow follows elevation.

  • Environment / IBL swaps with light/dark mode (brighter neutral env in

    light; dimmer in dark). Background defaults to transparent (inherits surface) — never a baked-in colour.

R5 — AR mode

"View in your space" button, shown only when supported:

Platform Mechanism
Android Scene Viewer (GLB)
iOS / iPadOS / visionOS AR Quick Look (USDZ)
WebXR browsers WebXR session (opt-in)
Unsupported Button hidden — never a dead control

AR is not themed (uses OS-native UI) and is never the only way to see the model.

R6 — Accessibility

  • aria-label / alt REQUIRED — a text description of the model

    (the model is never the sole carrier of information; pair with caption or spec text).

  • Keyboard orbit via arrow keys; viewer is focusable with a visible ring.
  • Poster image has its own alt.
  • prefers-reduced-motion → no auto-rotate, no entry animation; the

    poster + manual orbit remain.

  • Respects forced-colors — the chrome (buttons, progress) falls back

    to system colours; the 3D canvas is exempt.

R7 — Platform binding (koder_kit)

  • Web / Flutter Web<koder-model-viewer src= poster= alt=>

    wrapping the self-hosted <model-viewer> element.

  • Flutter mobile/desktopKoderModelViewer widget (Filament /

    model_viewer_plus backend); same poster-first + AR contract.

  • Shared attributes: src, usdz, poster, alt, auto-rotate,

    ar, camera-controls, max-zoom.

R8 — Forbidden patterns

  • ❌ No poster (blank box during load / on failure).
  • ❌ Auto-rotate that ignores prefers-reduced-motion or never stops.
  • ❌ Model as the only source of essential info (R6).
  • ❌ Unoptimized multi-MB GLB shipped without Draco/meshopt (R3).
  • ❌ Third-party model-hosting SaaS or CDN runtime (R1, self-hosted).
  • ❌ Baked background colour that ignores light/dark (R4).
  • ❌ Blocking the main thread on load instead of lazy + progress (R3).

T1-T5 — Tests

T1 — Poster fallback: with JS disabled / before activation, the poster renders at the correct aspect with valid alt. T2 — Lazy load: the GLB is not fetched until the element is in viewport or activated (network assertion). T3 — Reduced-motion: with prefers-reduced-motion, no auto-rotate and no entry animation occur. T4 — AR gating: AR button is absent on a platform reporting no AR support (no dead control). T5 — Lighting coherence: scene directional light angle equals --kds-light-azimuth/elevation within 1° (lighting.kmd § T3).

Live demo

<div data-kds-3d-demo"model-viewer" data-src"assetsmodelskoder-cube.gltf" data-poster="assetsmodelskoder-cube-poster.svg" data-aria-label="Interactive 3D model viewer: tap to load, drag to orbit, scroll to zoom. Poster-first; no auto-rotate under reduced-motion.">/div

  • themes/depth.kmd — the viewer sits on the z-axis like any surface
  • themes/lighting.kmd — shared light rig (R4) + cast shadow
  • themes/motion/physics.kmd — reduced-motion contract (R2/R6)
  • components/cards.kmd — product card hosting a model viewer (R5 media)
  • policies/self-hosted-first.kmd — bundled runtime, no SaaS (R1)