Model viewer
components specs/components/model-viewer.kmd
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.
Quando esta spec se aplica
Triggers primários
- Add a 3D model viewer to any Koder surface
Todos os triggers
- Show a 3D product / hardware / asset that the user can rotate
- Add an AR 'view in your space' affordance
- Embed a glTF/GLB/USDZ model on web, desktop or mobile
Corpo da especificação
Spec — Model viewer
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:
- Render the poster image immediately (correct aspect, no layout
shift — like
cards.kmd § R5). - Defer the viewer runtime + model until the element enters the viewport (IntersectionObserver) or the user activates the poster.
- 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/desktop —
KoderModelViewerwidget (Filament /model_viewer_plusbackend); 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-motionor 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
Cross-link
themes/depth.kmd— the viewer sits on the z-axis like any surfacethemes/lighting.kmd— shared light rig (R4) + cast shadowthemes/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)
Referências
specs/themes/depth.kmdspecs/themes/lighting.kmdspecs/themes/motion/physics.kmdspecs/components/cards.kmdspecs/policies/self-hosted-first.kmd