IFC Viewer SDK · v1.18.0
Embed the IFC viewer in your own product
Insert the same browser viewer into your CDE, digital twin, or internal project tools. Load IFC data straight from your app while every model is parsed on the client.
Three lines to first render
const viewer = new IfcViewer("#viewer");
await viewer.add("project.ifc", ifcBytes);
// → rendered, validated, all on the client- 100% client-side
- No upload backend
- Zero runtime deps
- ~6 KB wrapper
- TypeScript types
- 10 languages
- IDS-validated engine
Setup
Quick start
Pick your stack. Copy, paste, render — no build step, no API key.
<div id="viewer" style="height:520px"></div>
<script type="module">
import { IfcViewer } from "https://www.ifcvieweronline.eu/sdk/ifc-viewer.es.js";
const viewer = new IfcViewer("#viewer");
// Load IFC bytes from your app — nothing is uploaded
const bytes = await fetch("/models/project.ifc").then(r => r.arrayBuffer());
await viewer.add("project.ifc", bytes);
</script>Zero JavaScript — drop the tag into any page or dashboard and set attributes.
<script type="module" src="https://www.ifcvieweronline.eu/sdk/ifc-viewer.es.js"></script>
<ifc-viewer model="https://your-cde.com/model.ifc" ui="minimal" accent="#3fa866"
style="display:block;height:520px"></ifc-viewer>Mount in an effect, tear down on unmount. Safe under React 18 StrictMode double-invocation.
import { useEffect, useRef } from "react";
import { IfcViewer } from "https://www.ifcvieweronline.eu/sdk/ifc-viewer.es.js";
export function ModelViewer({ url }) {
const host = useRef(null);
useEffect(() => {
const viewer = new IfcViewer(host.current, { model: url });
return () => viewer.dispose(); // StrictMode-safe teardown
}, [url]);
return <div ref={host} style={{ height: 520 }} />;
}Mount on onMounted, dispose on onUnmounted.
<script setup>
import { onMounted, onUnmounted, ref } from "vue";
import { IfcViewer } from "https://www.ifcvieweronline.eu/sdk/ifc-viewer.es.js";
const host = ref(null);
let viewer;
onMounted(() => { viewer = new IfcViewer(host.value, { model: "https://host/m.ifc" }); });
onUnmounted(() => viewer && viewer.dispose());
</script>
<template>
<div ref="host" style="height: 520px" />
</template>Hosted vs. self-hosted
The snippets import from our CDN, which works on any origin. To self-host, serve ifc-viewer.es.js from your own domain — the SDK auto-discovers the app URL relative to the script. Serving the app elsewhere? Set the baseUrl option.
No npm package yet: import the module by URL (above) or vendor ifc-viewer.es.js into your app.
Proof
Live demo
This page is running the SDK. Click a control to fetch a sample IFC and drive the viewer — each button shows the call it makes.
⚠ The sample host may be rate-limiting. Try again in a moment.
Architecture
How it works
A tiny wrapper in your page, the full viewer in a sandboxed iframe, your bytes streamed across — and parsed where they already live.
Your app
A ~6 KB ES module that creates an iframe and a typed JS API. None of the heavy 3D / parsing weight touches your bundle.
postMessage bridge
IFC bytes are transferred to the iframe as an ArrayBuffer (zero-copy). Commands and queries are correlated request/response messages.
The viewer
three.js + web-ifc compiled to WebAssembly parse and render the model inside the iframe — on the visitor’s machine.
Why it’s private
Models never reach a server. No upload pipeline to secure, no data-residency question, no file-size queue.
Why it scales
Compute runs on each visitor’s device. Serving a 200 MB model costs the same as a 2 MB one — it’s a static file.
Concepts
Core concepts
Four things to know before you build.
Mount & ready
new IfcViewer(target) mounts immediately and queues commands until the viewer is ready. await IfcViewer.create(target) (or whenReady()) gives you an instance you can use straight away.
// queues commands until ready
const viewer = new IfcViewer("#viewer");
// …or await an instance that is ready to use
const viewer = await IfcViewer.create("#viewer");Bytes vs. URL
add(name, bytes) streams bytes you already have — no CORS, nothing uploaded; the buffer is transferred (detached), so pass a copy if you still need it. addFromUrl(url) fetches a public, CORS-enabled URL instead.
// you already have the bytes — no CORS, nothing uploaded
await viewer.add("a.ifc", bytes);
// …or load a public, CORS-enabled URL
await viewer.addFromUrl("https://host/b.ifc");Events
Subscribe with on(event, cb), which returns an unsubscribe function. You get ready, model-progress, model-loaded, validation-completed, model-error, element-selected, pointcloud-picked and map-feature-picked — the three things a scene is made of, on three events.
const off = viewer.on("model-loaded", (m) => {
console.log(m.fileName, m.elementCount);
});
// later: off();Queries
Pull data on demand: getModels, getElement, getValidation, getStats, getIssues, checkIds, checkEir, screenshot. Each is request/response and rejects after a timeout (30 s; IDS / EIR up to 120 s).
const models = await viewer.getModels();
const { qualityScore } = await viewer.getValidation();
const png = await viewer.screenshot();Reference
API reference
Every method, grouped — with signatures and return types. Constructor options, events and the <ifc-viewer> web component follow.
Constructor
new IfcViewer(target, options?)IfcViewerMount into a CSS selector or element. See the options below.
IfcViewer.create(target, options?)Promise<IfcViewer>Create a viewer and resolve once it is ready to accept commands.
Loading models
add(name, bytes)Promise<ModelLoadedEvent>Load IFC bytes (ArrayBuffer/Uint8Array) from your app. The buffer is transferred for a zero-copy hand-off.
addFromUrl(url, name?)Promise<ModelLoadedEvent>Load a public, CORS-enabled IFC URL.
removeModel(modelId)voidUnload a model by id.
clear()voidRemove all loaded models.
Camera & selection
select(expressId | globalId, modelId?)voidSelect and frame an element — by IFC expressID, or by GlobalId (which also finds its model).
setView(view)voidFly to a named view of the scene — iso, front, back, left, right, top, bottom — with all of it in frame. Fire-and-forget (between 1.14 and 1.17 the viewer ignored it).
frame({ view?, fill?, azimuth?, elevation?, animate? })Promise<{ scope }>Frame for presentation: the model fills `fill` of the frame (0.2–0.98, default 0.85), fitted to its corners rather than its bounding sphere. azimuth / elevation look from any angle.
frame(elementId, modelId?, options?)Promise<{ scope: 'element' }>Frame one element (expressID): from the current angle unless a view is given. Rejects when it is not in the model.
fit() · reset()voidFrame the active model from the current angle, with a margin / reset the camera.
setCamera(position, direction)voidPlace the camera at a position looking along a direction.
getCamera()Promise<CameraState | null>Where the camera is and what it looks at — save it, restore it with lookAt.
lookAt(position, target, animate?)Promise<void>Fly the camera to a position, looking at a target (scene metres, Y up).
setWalkMode(enabled, { speed? })Promise<WalkState>First-person walk mode (WASD, drag to look, Esc to leave). Emits walk-changed.
getWalkState()Promise<WalkState>Whether walk mode is on, and at what speed.
Visibility
isolate(ifcType?)voidIsolate a category (e.g. "IfcWall"); omit to clear.
showAll()voidRestore full visibility (clear hidden + isolation).
hideElements(ids, modelId?) · showElements(ids, modelId?)voidHide / show a set of elements by expressID.
setModelVisible(modelId, visible)Promise<void>Show or hide one model of a federated scene without unloading it.
setModelOpacity(opacity, modelId?)Promise<void>Ghost a model (0.05–1) — e.g. the architecture around the MEP.
isolateModel(modelId | null)Promise<void>Show only this model; null shows them all again.
Look & presentation
setBackground(background)Promise<BackgroundState>Change the scene background: a preset ('white', 'paper', 'blueprint', 'sky', 'studio'), '#rrggbb', '#top,#bottom' or { top, bottom }. Not saved as the visitor's own preference.
getBackground()Promise<BackgroundState>The current scene background.
setAccent(color)Promise<void>Re-theme the UI accent at runtime (#rrggbb).
setClientMode(enabled)Promise<void>Switch the client skin on or off — the stakeholder view, same as ui: 'client'.
setRenderQuality(quality)Promise<void>'quality' turns on the heavier rendering for a hero shot; 'standard' for everyday.
Sun study
setSolar(opts?)Promise<SolarState>Real shadows at a site-local date and time: { date: '06-21', time: '18:00' }. The site comes from the IFC georeference; pass location for a model without one — never a silent default city.
getSolar()Promise<SolarState>Date, time and time zone of the study, and where the site is.
Map mode
setSiteContext(opts?)Promise<SiteContextState>Put the model on the map with terrain and OpenStreetMap surroundings. Calling it is your page declaring tile consent for its visitors; show the attributions.
getSiteContext()Promise<SiteContextState>Map mode state, the placement, and the attributions you must display.
Scenes
openScene(url | SceneDocument)Promise<void>Open another scene: a .scene.json URL, or a scene document (it travels packed in the frame address, so it must fit a link). The viewer reloads with it; waiting calls are rejected and ready fires again.
exportScene({ title?, description?, camera? })Promise<SceneExport>The scene on screen as a scene document — models by URL, layers, twin, map, camera — with the sources to credit and a link that carries it.
packScene(doc)Promise<string | null>Pack a scene document for a #scene= link yourself (null when too big for a link).
Data layers and twin
getLayerPresets()Promise<LayerPreset[]>The catalogue of live public sources, named in the viewer language, with what each needs in a browser (a key, a proxy) and whether it has data near the site.
addLayer({ preset } | { url, live? } | { geojson })Promise<DataLayerInfo>Add a layer: a catalogue source ({ preset }), GeoJSON or a live feed at a URL ({ url, live }), or GeoJSON you already have ({ geojson }). Session-only: never saved into the visitor’s layers.
getLayers()Promise<DataLayerInfo[]>Every data layer: status, feature counts, extent, refresh and attribution.
setLayerVisible(id, visible)Promise<DataLayerInfo>Show or hide a layer.
frameLayer(id)Promise<void>Fly the camera to a layer’s features.
removeLayer(id)Promise<void>Remove a layer.
getTwin()Promise<TwinState>The operational twin now: device sources (state, last reading) and, per binding, the matching rule, its colour, the label value and whether it is alerting.
Sections
addSection({ axis?, offset?, level?, flip? })Promise<SectionsState & { id }>Add a section plane. { level: 'Level 1' } is a floor plan at that storey; { axis: 'x', offset: 4.5 } a section at 4.5 m.
updateSection(id, { offset?, enabled?, flipped? })Promise<SectionsState>Move, toggle or flip a plane.
removeSection(id?)Promise<SectionsState>Remove one plane, or every cut when no id is given.
setSectionBox('model' | 'selection' | false)Promise<SectionsState>A section box around the model or the selection; false removes it.
getSections()Promise<SectionsState>Every plane, the box, and the storeys available for level cuts.
Tours
startTour(template?, { autoplay?, title? })Promise<TourState>Start a built-in tour: 'social', 'client-walkthrough' or 'technical-review' (walks the validation issues). Played by the viewer's own tour bar.
playTour({ title?, steps }, { startAt?, autoplay? })Promise<TourState>Play a tour you authored: camera stops with captions, highlights and isolated classes. autoplay: true or ms per stop.
nextTourStep() · prevTourStep() · goToTourStep(i)Promise<TourState>Move between stops.
setTourAutoplay(autoplay)Promise<TourState>Turn self-running on or off for the tour playing now.
stopTour()Promise<TourState>Stop the tour and give the camera back.
getTour()Promise<TourState>The tour and its stops, in the shape playTour() takes — save it, replay it later.
Presentation director
getPresentationRecipes()Promise<PresentationRecipe[]>The built-in director recipes (meeting demo, reel, LinkedIn teaser, launch, editorial looks…).
createPresentation(recipe?, options?)Promise<PresentationState>Turn the model into an edited video from a recipe: shots planned from the IFC, captions, music. Opens Clip Studio with the result. Follow presentation-progress.
exportPresentation({ resolution?, music? })Promise<PresentationVideo>Encode the presentation in the visitor’s browser and receive the MP4 bytes — upload them, attach them, or play them in a <video>.
closePresentation()Promise<void>Close Clip Studio.
Cover Studio
getCoverOptions()Promise<CoverCatalog>The recipes, templates, formats and palettes Cover Studio offers.
createCover({ recipe?, template?, format?, palette?, text? })Promise<CoverState>Make a cover: run a recipe (it captures the views it needs), then set template, format, palette and texts. Opens Cover Studio, still editable.
getCover()Promise<CoverState | null>The cover as it stands, or null when Cover Studio is closed.
exportCover({ type?, slide? })Promise<CoverFile>Export it: png / jpeg (one page), pdf / pptx (the whole document), zip (every page). Returns the bytes.
closeCover()Promise<void>Close Cover Studio.
Article kit
activate()voidBoot a lazy viewer now (what the poster button does). Calls made before were queued.
bindSteps(steps, { rootMargin? })() => voidScrollytelling: while each paragraph crosses the middle of the screen, frame, isolate, move the sun or run your own code. Returns an unbind function.
setTurntable(enabled | degPerSec)Promise<{ active, speed }>A slow idle orbit (degrees per second; false stops it). Stops at the first touch; never under reduced motion.
toggleFullscreen()Promise<void>Expand the figure to the whole screen, or back.
setPaused(paused)Promise<{ paused }>Stop or resume painting frames. pauseOffscreen does it for you.
Scene groups
getGroups()Promise<SceneGroupsState>Every group in the scene — inferred from the IFC (project, site, location) and made by users.
createGroup(name, modelIds?)Promise<string>Create a group, optionally with models in it. Resolves with its id.
renameGroup(id, name) · deleteGroup(id)Promise<void>Rename or delete a user group. Automatic groups cannot be edited.
assignToGroup(itemId, groupId | null | 'loose')Promise<void>Move a model or point cloud into a group; null gives it back to automatic grouping, 'loose' leaves it in none.
setGroupVisible(id, visible) · isolateGroup(id | null)Promise<void>Show or hide a whole group, or show only one.
frameGroup(id)Promise<void>Fit the camera to a group — hidden members included.
Measurements
setMeasureTool(tool)Promise<void>Arm a measuring tool for the visitor (opens the Measure panel), or 'none'. Results arrive on measurements-changed.
getMeasurements()Promise<MeasurementsState>Every measurement on screen. Values are SI — metres, m², degrees — whatever the viewer displays.
clearMeasurements(id?)Promise<MeasurementsState>Remove one measurement, or all of them.
Queries
getModels()Promise<ModelSummary[]>List the loaded models. Returns a promise.
findElements({ ifcClass?, globalId?, name?, modelId?, limit? })Promise<ElementSummary[]>Find elements by IFC class, GlobalId and/or name across the loaded models. Each hit has what getElement(), select() and frame() take.
getElement(expressId | globalId, modelId?)Promise<IfcElementData | null>By expressID or GlobalId. An element’s IFC data: attributes with the GlobalId, its property sets and quantities, and its type’s — typeName, typeProperties — with units. effectivePropertySets merges both, the element’s own value winning.
validate(modelId?, { force? })Promise<ValidationRunResult>Validate a model now (default: the active one) and resolve with its Health Score and issue counts. Rejects with the reason.
getValidation()Promise<ValidationSummary | null>The validation result on screen, or null before any run has produced one. See “Validation lifecycle”.
getValidationStatus()Promise<ValidationStatus>Where validation stands right now: idle, running (with progress), done or error.
getStats()Promise<StatsResult>Per-category element counts per model — for dashboard charts. Returns a promise.
getIssues({ severity?, limit?, modelId? })Promise<IssuesResult>Validation issues for a table (filter by severity / limit). Returns a promise.
screenshot()Promise<string>Capture the current 3D view as a PNG data URL. Returns a promise.
compare({ base, head, baseLabel?, headLabel? })Promise<{ changes }>Compare two deliveries by URL (by GlobalId) and open the comparison workspace on the result.
Panels
openPanel(panel | null) · closePanel()voidOpen a tool panel, or pass null to close whatever is open. A panel that is not available — the chrome hides it, or nothing is loaded for it to act on — is a no-op, not an error.
getPanels()Promise<PanelsResult>Which panel is open, and which are on offer right now. A tool missing from `available` cannot be opened; it is not merely disabled.
setPanels(panels)voidLimit the rail to these panels, at runtime. Same vocabulary as the `panels=` URL parameter and it outranks it. Narrows what the viewer offers and never adds; an empty array means no rail.
Point clouds
addPointCloud(fileName, bytes)Promise<string>Load a scan from bytes — LAS, LAZ, COPC, PLY, PCD or text. The buffer is transferred, not copied. Resolves with the cloud id.
addPointCloudFromUrl(url, fileName?)Promise<string>Load a scan the viewer fetches itself. The URL must allow CORS.
listPointClouds()Promise<PointCloudInfo[]>Every loaded scan. `pointCount` is what is resident; `declaredCount` is what the file holds — they differ when the budget truncated the parse.
removePointCloud(cloudId) · clearPointClouds()Promise<void>Unload one scan and free its GPU buffers, or unload them all.
setPointCloudVisible(cloudId, visible)Promise<void>Show or hide a scan without unloading it.
fitPointCloud(cloudId?)Promise<void>Frame the camera on a scan, or on the first one loaded.
setPointCloudDisplay(display, renderBudget?)Promise<void>Point size, opacity, colour mode, density, confidence cut-off — shared by every scan. Each is a shader uniform, so it is instant on 20 million points.
inspectPointCloud(enabled?)Promise<void>Arm click-to-read. While armed, clicking a point emits `pointcloud-picked` with the coordinates IN THE FILE alongside the scene ones.
setPointCloudPlacement(placement, cloudId?)Promise<void>Nudge a scan by hand: position, yaw, levelling, scale. Partial and clamped; sits on top of the derived alignment rather than replacing it.
setPointCloudUpAxis(axis, cloudId?)Promise<void>Correct which axis the scan treats as up. PLY, PCD and text declare no orientation, so the viewer infers it — check `upAxisSource` before trusting it.
Imported 3D models
addMesh(files)Promise<string>Import a model from bytes. Takes a LIST: a .gltf needs its .bin and textures, an .obj its .mtl — send the entry file alone and you get grey geometry.
addMeshFromUrl(urls)Promise<string>Import a model the viewer fetches. Pass every URL it needs; they are fetched in parallel and all must allow CORS.
listMeshes()Promise<MeshInfo[]>Every imported model, with its triangle and texture counts. Read `unitSource` and `upAxisSource` before trusting the values beside them.
removeMesh(meshId?) · clearMeshes()Promise<void>Remove an import and free its geometry, materials and textures, or remove them all.
setMeshVisible(visible, meshId?)Promise<void>Show or hide an import without unloading it.
fitMesh(meshId?)Promise<void>Frame the camera on an import, or on all of them.
setMeshPlacement(placement, meshId?)Promise<void>Place an import by hand: position, yaw, levelling, scale. Partial and clamped. Imports start centred on the IFC, sitting on its floor.
setMeshUpAxis(axis, meshId?)Promise<void>Correct the source's vertical axis. Only meaningful for OBJ — glTF's specification mandates Y-up, so it reports `upAxisSource: 'declared'`.
setMeshUnit(unitScale, meshId?)Promise<void>Correct the source unit: 1 metres, 0.01 centimetres, 0.001 millimetres, 0.3048 feet. None of these formats records one, so the viewer infers it from the model's size.
IDS & EIR compliance
checkIds(idsXml)Promise<IdsResult>Check the model against a buildingSMART IDS (.ids XML). Runs in a worker. Returns pass/fail per spec.
checkEir(profile)Promise<IdsResult>Check the model against an EIR / BIM Validation profile — an object, its JSON (incl. the compact shorthand) or a built-in id such as 'builtin-en14351-1'. Compiles to IDS and runs on the same engine: same IdsResult shape as checkIds.
Languages
setLanguage(lang)voidChange the UI language at runtime.
getLanguages()string[]List the supported language codes (see IfcViewer.LANGUAGES for labels).
Lifecycle & events
whenReady()Promise<void>Resolves when the viewer is ready to accept commands.
isReadybooleanGetter — true once the viewer has signalled readiness.
on(event, cb)() => voidSubscribe to an event; returns an unsubscribe function.
off(event, cb)voidRemove a previously added event listener.
dispose()voidTear down the viewer and remove the iframe.
Concurrent add() / addFromUrl() calls are serialized internally and each promise is correlated to its own load — an app-initiated load (URL param, in-iframe upload) never resolves your add() promise.
Constructor options
| Option | Type | Default | Description |
|---|---|---|---|
ui | 'minimal' | 'full' | 'kiosk' | 'client' | 'article' | 'embed' | 'minimal' | Chrome preset. 'embed': for a host app that supplies the file — 3D, properties on selection, a compact toolbar with Validate and Measure; no Open, Share, account or language. |
toolbar | boolean | preset | Show the toolbar (default: the preset’s). false leaves the 3D view and the panels. |
tools | ('validate' | 'measure')[] | ['validate', 'measure'] | Tools on the compact toolbar of ui: 'embed' — 'validate', 'measure'. [] for none. |
autoFrame | boolean | true | Frame the whole scene from iso, with a margin, every time a load settles. false leaves the camera to you. |
validate | boolean | true | Run validation on load (drives the Health Score). |
panel | boolean | false | Auto-open the validation panel. |
panels | PanelName[] | — | Limit the tool rail to these panels, from the first frame — `['scene','map']`, or `[]` for no rail. Same names as setPanels and the `panels=` URL parameter. Prefer it over calling setPanels after load, which shows the full rail first and then takes tools away. |
lang | string | auto | Force the UI language (e.g. en, es, ja). |
accent | #rrggbb | brand | Tint the viewer to match your dashboard. |
model | string | — | Auto-load this public IFC URL once ready. |
background | BackgroundSpec | — | Scene background from the first frame — a preset, '#rrggbb' or '#top,#bottom'. |
map | boolean | ('terrain' | 'buildings' | 'showcase')[] | — | Put the model on the map once loaded: true, or layers ['terrain','buildings','showcase']. |
solar · moon | 'MM-DDTHH:MM' · boolean | — | Open the sun study at a site-local time: '06-21T18:00'. |
scans | string[] | — | Point cloud URLs to load alongside the model. |
layers | string | — | Data-layer setup to open: the URL of a JSON exported from the Data layers panel (live sources, styles, alerts). Since 1.16. |
scene | string | — | Scene to open: the URL of a scene document (models, data layers, live devices, map, camera). Since 1.17. |
view · fill | CameraView · number | — | Once every model has loaded, frame it from this view with a tight fit. ui: 'article' implies 'iso'. |
wheel | 'always' | 'ctrl' | 'always' | 'ctrl': the wheel scrolls your page and zooms only with Ctrl/⌘ held, like an embedded map. ui: 'article' implies it. |
lazy | boolean | 'visible' | false | Load nothing until wanted: true shows a poster with a button, 'visible' boots when the figure nears the screen. |
poster · posterTitle · posterText · launchLabel | string | — | Poster image, title, text and button label shown until the viewer is up. |
aspectRatio | string ('16/10') | — | Size by proportion ('16/10'): poster and viewer share the box, so nothing moves on the page. |
turntable | boolean | number | false | Slow idle orbit once loaded (true = 6°/s, or degrees per second). |
pauseOffscreen | boolean | ui === 'article' | Stop rendering while off screen. Default true with ui: 'article'. |
fullscreenButton | boolean | false | A small expand button in the corner. |
height · width | number | string | '100%' | iframe size (number → px). |
baseUrl | string | auto | App base URL. Auto-derived from the script URL. |
loadTimeout | number | 120000 | Reject add()/addFromUrl() after N ms (0 disables). |
className · title | string | — | Extra class / iframe title. |
on* callbacks | function | — | Convenience callbacks — same as .on(...). |
Events
| Event | Payload | Description |
|---|---|---|
ready | { languages } | Viewer mounted and ready. |
model-progress | { percent, phase } | Load progress (download → parse → render). |
model-loaded | { modelId, fileName, elementCount, fromCache } | A model finished loading. |
validation-started | { modelId } | A validation began on a model — after a load (validate: true) or from validate(). |
validation-completed | { modelId, qualityScore, errors, warnings, info, total } | Validation finished: the Health Score and counts, and the model they are about (null for a federated aggregate). |
validation-failed | { modelId, message } | A validation could not run or did not finish — with the reason. |
model-error | { message, url?, name? } | A load failed. |
element-selected | { expressId, modelId, ifcType, name, globalId } | The user picked an element — with its GlobalId, to match your own records. |
pointcloud-picked | { cloudId, position, sourcePosition, classification, intensity, distance } | A point was read off a scan. Carries the file's own coordinates, not the scene's. |
map-feature-picked | { id, name?, label?, featureKind, heightM?, heightEstimated } | A building in the OpenStreetMap surroundings was clicked. Context, not model. |
walk-changed | { active, speed } | Walk mode turned on or off — by the visitor or by the host. |
measurements-changed | { tool, units, items } | A measurement was added, removed or renamed. Carries the whole list. |
tour-started | { title, total, template } | A tour began playing. |
tour-step | { index, total, caption } | The tour moved to another stop. |
tour-ended | { completed } | The tour stopped; completed when it reached the last stop. |
presentation-progress | { stage, label?, progress } | The director is generating or exporting a presentation. |
layer-feature-picked | { layerId, layer, featureId, geometry, lonLat, properties } | A feature of a data layer was clicked: its layer, properties and a point to mark. |
alert | { kind, from, id, name, rule, count, sample } | A data-layer or twin rule started or stopped alerting. |
Statics
IfcViewer.LANGUAGES ({ code, label }[], native names) and IfcViewer.SUPPORTED_LANGUAGES (codes) — build a language picker before the viewer is ready.
Web component
Zero JavaScript — drop the tag into any page or dashboard and set attributes.
Attributes: modeluilangaccentvalidatepanelbase-urltoolbartoolsauto-frame
Events re-dispatch as DOM CustomEvents named ifcviewer:<type> (detail = payload). The underlying IfcViewer is on the element’s .viewer; getStats / getIssues / screenshot proxy through.
const el = document.querySelector("ifc-viewer");
el.addEventListener("ifcviewer:validation-completed", (e) => {
updateScore(e.detail.qualityScore);
});
const stats = await el.getStats();Guides
Recipes
Copy-paste patterns for the things teams actually build.
Element inspector
Click in 3D, read the IFC property sets into your own panel.
viewer.on("element-selected", async (e) => {
const data = await viewer.getElement(e.expressId, e.modelId);
renderPanel(data.name, data.globalId, data.propertySets);
});Health Score widget
Surface the model’s quality score and issue counts in your dashboard.
viewer.on("validation-completed", (v) => {
setScore(v.qualityScore); // 0–100, or null
setCounts(v.errors, v.warnings, v.info);
});IDS compliance check
Validate against a buildingSMART IDS and list pass / fail per spec.
const ids = await fetch("/specs/project.ids").then(r => r.text());
const res = await viewer.checkIds(ids); // runs in a worker
res.specs.forEach((spec) => {
row(spec.name, spec.status, spec.passedCount + "/" + spec.applicableCount);
spec.failures.forEach((f) => onClick(() => viewer.select(f.expressId, f.modelId)));
});EIR / BIM Validation
Run an editable EIR profile (ISO 19650-style) — no .ids file needed. Failures carry the element GlobalId.
// An editable EIR profile — object, JSON, or the compact shorthand below.
const profile = {
name: "Door QA",
rules: [
{ type: "requiredProperty", entity: "IfcDoor", property: "FireRating", severity: "error" },
{ type: "allowedValues", entity: "IfcDoor", property: "FireRating",
values: ["EI30", "EI60", "EI90"], severity: "warning" },
],
};
const res = await viewer.checkEir(profile); // same IdsResult as checkIds
res.specs.forEach((spec) => {
spec.failures.forEach((f) => row(spec.name, f.globalId, f.reasons.join(" · ")));
});A scene for a blog post
White background, the model on its site, and the evening shadow of the summer solstice — declared up front, no clicks.
// Declared up front: the iframe boots with all of it, no clicks, no flash.
const viewer = await IfcViewer.create("#viewer", {
model: "https://example.com/house.ifc",
ui: "client",
background: "white",
map: ["terrain", "buildings"], // your page declares tile consent
solar: "06-21T19:30", // summer solstice, site-local
});
// …or change the story as the reader scrolls
await viewer.setSolar({ date: "12-21", time: "09:30" });
const { attributions } = await viewer.getSiteContext(); // show theseFloor plan and measurements
Cut a plan at a named storey, then let the reader measure — and read the results back in SI units.
const { levels } = await viewer.getSections();
await viewer.addSection({ level: levels[0].name }); // floor plan, 1.2 m up
viewer.setView("top");
await viewer.setMeasureTool("distance");
viewer.on("measurements-changed", ({ items }) => {
// value is SI — metres, m², degrees — whatever the viewer displays
render(items.map((m) => m.kind + ": " + m.value?.toFixed(2)));
});A guided tour in an article
Author the stops once with getCamera(), then let the tour run by itself next to your text.
// 1. Once, while writing the post: frame each view and copy the camera.
console.log(JSON.stringify(await viewer.getCamera()));
// 2. In the page: play the stops, self-running, next to your text.
await viewer.playTour({
title: "Casa Poblenou",
steps: [
{ position: { x: 52, y: 29, z: 24 }, target: { x: 18, y: 7, z: -11 }, caption: "The site" },
{ position: { x: 18, y: 40, z: 11 }, target: { x: 18, y: 0, z: 10 },
caption: "Structure", isolate: ["IfcColumn", "IfcSlab"] },
],
}, { autoplay: 6000 });
viewer.on("tour-step", ({ index, caption }) => highlightParagraph(index));A video of the model, from your page
Generate a presentation with a recipe and get the MP4 back — no screen recording, no upload.
viewer.on("presentation-progress", ({ stage, progress }) => bar.value = progress ?? 0);
await viewer.createPresentation("linkedin-teaser", { title: "Casa Poblenou", targetSec: 20 });
const { bytes, mimeType } = await viewer.exportPresentation({ resolution: 1080 });
video.src = URL.createObjectURL(new Blob([bytes], { type: mimeType }));A cover for your post
Generate a Pinterest pin or a LinkedIn card of the model, with your title, and get the PNG back.
await viewer.createCover({
recipe: "pinterest",
text: { title: "Casa Poblenou", location: "Barcelona", website: "https://example.com" },
});
const { bytes, mimeType } = await viewer.exportCover({ type: "png" });
img.src = URL.createObjectURL(new Blob([bytes], { type: mimeType }));A model inside an article
ui: 'article' is the canvas alone, framed so the model fills it, with a wheel that lets the reader keep scrolling. Pick the angle with frame().
const viewer = new IfcViewer("#figure", {
ui: "article", // canvas only, tight iso framing, wheel scrolls the page
model: "https://example.com/pavilion.ifc",
lazy: true, // nothing loads until the reader asks
poster: "/img/pavilion.jpg",
posterTitle: "The pavilion, live",
aspectRatio: "16/10", // no layout shift from poster to viewer
background: "auto", // paper on a light page, studio on a dark one
turntable: true, // a slow turn until the reader takes over
fullscreenButton: true,
validate: false,
});A story that moves with the text
Pin the viewer and let each paragraph set the shot: isolate the walls, swing round, bring in the evening sun.
// <div id="figure" style="position:sticky;top:1rem"></div>
// <p id="s1">…</p> <p id="s2">…</p> <p id="s3">…</p>
const viewer = new IfcViewer("#figure", { ui: "article", aspectRatio: "16/10", model: url });
viewer.bindSteps([
{ el: "#s1", frame: { view: "iso" } },
{ el: "#s2", isolate: "IfcWall", frame: { azimuth: 225, elevation: 20 } },
{ el: "#s3", isolate: null, solar: { active: true, date: "06-21", time: "19:30" } },
]);A live city twin in your page
Open a published scene, react to its alerts and clicks, add your own live source next to it, and hand out a link to the result.
const viewer = new IfcViewer("#twin", {
ui: "client",
scene: "https://www.ifcvieweronline.eu/scenes/barcelona-placa-catalunya.scene.json",
});
viewer.on("alert", (a) => { if (a.kind === "start") notify(a.name + ": " + a.rule) });
viewer.on("layer-feature-picked", (f) => showCard(f.layer, f.properties));
// Your own live source next to the scene's (GeoJSON with CORS, every 60 s)
await viewer.addLayer({ url: "https://example.org/sensors.geojson", live: 60 });
const { link, sources } = await viewer.exportScene({ title: "Our twin" });A product sheet in your app
Pin the version, hand over the file, and show what the manufacturer put in the type: U-value, wind load class, manufacturer.
import { IfcViewer } from "https://www.ifcvieweronline.eu/sdk/1.18.0/ifc-viewer.es.js";
const viewer = await IfcViewer.create("#product-3d", {
ui: "embed", // 3D + properties, Validate and Measure only
panels: ["properties"],
lang: "es",
});
const bytes = await fetch(product.ifcUrl).then(r => r.arrayBuffer());
await viewer.add("V-70-PR.ifc", bytes); // framed for you
// The product, by class (or by its GlobalId) — no expressIDs to know
const [win] = await viewer.findElements({ ifcClass: "IfcWindow" });
// What the manufacturer put in the type (IfcWindowType → HasPropertySets)
const el = await viewer.getElement(win.globalId);
const pset = (name) => el.effectivePropertySets.find(s => s.name === name);
const uw = pset("Pset_WindowCommon")?.properties.find(p => p.name === "ThermalTransmittance");
console.log(el.typeName, el.globalId, uw?.value, uw?.unit); // "Ventana V-70 practicable" … 1.2 "W/(m²·K)"
const { qualityScore } = await viewer.validate(); // Health Score, on demand
// Declared performance per EN 14351-1: wind, water, acoustics, Uw, g / τv, DoP
const en = await viewer.checkEir("builtin-en14351-1");
const missing = en.specs.filter(s => s.status === "fail").map(s => s.name);Theme to your brand
Match the viewer to your product with one accent colour.
new IfcViewer("#viewer", { accent: "#3fa866" });
// …or on the web component:
// <ifc-viewer accent="#3fa866" model="…"></ifc-viewer>Language picker
Build a selector from the languages the viewer ships with.
// build a <select> from the bundled languages
IfcViewer.LANGUAGES.forEach(({ code, label }) => addOption(code, label));
// switch at runtime
viewer.setLanguage("ja");Health Score
Validation lifecycle
validate: true validates each model once it has loaded; validate() runs it on demand and resolves with the result. What getValidation() returns, moment by moment:
| When | getValidation() returns |
|---|---|
| Before any validation | null — getValidationStatus() says idle. |
| While the first run is going | null — getValidationStatus() says running, with progress; validation-started has fired. |
| After a run | { qualityScore, errors, warnings, info, total, status: 'done', modelId }. With several models, the aggregate (modelId: null). |
| While a newer run is going | The previous numbers, with status: 'running'. |
| After a run failed | The previous numbers with status: 'error' and error — or null if there were none. validation-failed has fired and validate() rejected. |
Versions
Pin a version
From 1.18.0, every release is also published at a path that never changes. Pin it in production; the moving path follows the latest release.
<script type="module">
import { IfcViewer } from "https://www.ifcvieweronline.eu/sdk/1.18.0/ifc-viewer.es.js";
</script>| Path | Caching | Use |
|---|---|---|
/sdk/ifc-viewer.es.js | revalidated on every load | Trying things out — always the latest. |
/sdk/1.18.0/ifc-viewer.es.js | immutable, cached for a year | Production — the code you tested is the code that runs. |
Which viewer does a pinned SDK open?
The iframe is the live viewer at the same origin, not a frozen copy: a pinned SDK keeps its own API and behaviour while the viewer inside keeps improving. The viewer answers every command any published SDK sends — the build checks it — so an old pin keeps working.
Pin the content too
versions.json, next to the SDK, lists each version’s Subresource Integrity hash:
<script type="module" src="https://www.ifcvieweronline.eu/sdk/1.18.0/ifc-viewer.es.js"
integrity="sha384-WsT4d0DS1oqva4rLWCGBDrx50uZVA3B95IDWbfxaFt8lnjs6NgOrjKNcWQL57/0z" crossorigin="anonymous"></script>Changelog
1.18.0 — catalogue objects in a host app. getElement() returns the GlobalId and the type’s data (typeName, typeProperties with units, effectivePropertySets; IFC4 types and IFC2x3 styles). validate(), getValidationStatus(), validation-started / validation-failed, and validate: true really validates after each load. Auto-frame after load, fit() and frame() with a margin for small objects, frame(elementId), and setView() moves the camera again. ui: 'embed' with toolbar and tools. Pinned builds at /sdk/1.18.0/. findElements(), GlobalIds for getElement() / select() / element-selected, window and door lining and panel sets, and the type's materials; in ui: 'embed' the properties panel keeps the picked object in view. checkEir('builtin-en14351-1') checks the declared performance of EN 14351-1 windows and external doors — wind load, watertightness, acoustics, U-value, g and τv, manufacturer and product code — on standard IFC property sets; EIR rules gain where (only elements with a property value) and optional.
Help
Troubleshooting
The handful of things that trip people up.
Nothing renders
Check, in order: the viewer reached ready, the model loaded (model-loaded vs model-error), the container has a non-zero height, and the byte source is valid IFC. The model-error message names the cause.
I get a CORS error loading a model
CORS only affects addFromUrl(url). If you control the bytes, fetch them yourself and call add(name, bytes) — no CORS applies because you hand the data over directly.
The iframe is blank or refuses to load
The app must be embeddable — no X-Frame-Options: DENY and no restrictive frame-ancestors CSP on the host. Our hosted app allows embedding.
My ArrayBuffer is empty after add()
add() transfers the buffer for a zero-copy hand-off, which detaches it in your code. Pass a copy (bytes.slice(0)) if you still need the bytes afterwards.
Loads or queries time out
add()/addFromUrl() reject after loadTimeout (default 120 s; 0 disables). Queries reject after 30 s, checkIds after 120 s. Large models on slow devices may need a higher loadTimeout.
Which browsers are supported?
Any modern evergreen browser with WebAssembly and ES module support — Chrome, Edge, Firefox, Safari. No Internet Explorer.
It 404s on my local dev server
A SPA dev server may intercept /sdk/ with its HTML fallback. Test against a real build / static host, or import the /sdk/ifc-viewer.es.js module directly.