IFC Viewer SDK v1.18.0 Open viewer
English

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

JavaScript
const viewer = new IfcViewer("#viewer");

await viewer.add("project.ifc", ifcBytes);
// → rendered, validated, all on the client

Setup

Quick start

Pick your stack. Copy, paste, render — no build step, no API key.

HTML
<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>

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.

idle
Last call —

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.

Visitor's browser — nothing uploadedYour appSDK · ~6 KBSandboxed iframethree.js · web-ifc · WASMIFC bytespostMessage

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.

JavaScript
// 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.

JavaScript
// 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.

JavaScript
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).

JavaScript
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?)IfcViewer

Mount 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)void

Unload a model by id.

clear()void

Remove all loaded models.

Camera & selection

select(expressId | globalId, modelId?)void

Select and frame an element — by IFC expressID, or by GlobalId (which also finds its model).

setView(view)void

Fly 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()void

Frame the active model from the current angle, with a margin / reset the camera.

setCamera(position, direction)void

Place 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?)void

Isolate a category (e.g. "IfcWall"); omit to clear.

showAll()void

Restore full visibility (clear hidden + isolation).

hideElements(ids, modelId?) · showElements(ids, modelId?)void

Hide / 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()void

Boot a lazy viewer now (what the poster button does). Calls made before were queued.

bindSteps(steps, { rootMargin? })() => void

Scrollytelling: 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()void

Open 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)void

Limit 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)void

Change 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.

isReadyboolean

Getter — true once the viewer has signalled readiness.

on(event, cb)() => void

Subscribe to an event; returns an unsubscribe function.

off(event, cb)void

Remove a previously added event listener.

dispose()void

Tear 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

OptionTypeDefaultDescription
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.
toolbarbooleanpresetShow 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.
autoFramebooleantrueFrame the whole scene from iso, with a margin, every time a load settles. false leaves the camera to you.
validatebooleantrueRun validation on load (drives the Health Score).
panelbooleanfalseAuto-open the validation panel.
panelsPanelName[]—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.
langstringautoForce the UI language (e.g. en, es, ja).
accent#rrggbbbrandTint the viewer to match your dashboard.
modelstring—Auto-load this public IFC URL once ready.
backgroundBackgroundSpec—Scene background from the first frame — a preset, '#rrggbb' or '#top,#bottom'.
mapboolean | ('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'.
scansstring[]—Point cloud URLs to load alongside the model.
layersstring—Data-layer setup to open: the URL of a JSON exported from the Data layers panel (live sources, styles, alerts). Since 1.16.
scenestring—Scene to open: the URL of a scene document (models, data layers, live devices, map, camera). Since 1.17.
view · fillCameraView · 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.
lazyboolean | 'visible'falseLoad nothing until wanted: true shows a poster with a button, 'visible' boots when the figure nears the screen.
poster · posterTitle · posterText · launchLabelstring—Poster image, title, text and button label shown until the viewer is up.
aspectRatiostring ('16/10')—Size by proportion ('16/10'): poster and viewer share the box, so nothing moves on the page.
turntableboolean | numberfalseSlow idle orbit once loaded (true = 6°/s, or degrees per second).
pauseOffscreenbooleanui === 'article'Stop rendering while off screen. Default true with ui: 'article'.
fullscreenButtonbooleanfalseA small expand button in the corner.
height · widthnumber | string'100%'iframe size (number → px).
baseUrlstringautoApp base URL. Auto-derived from the script URL.
loadTimeoutnumber120000Reject add()/addFromUrl() after N ms (0 disables).
className · titlestring—Extra class / iframe title.
on* callbacksfunction—Convenience callbacks — same as .on(...).

Events

EventPayloadDescription
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.

JavaScript
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.

JavaScript
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.

JavaScript
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.

JavaScript
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.

JavaScript
// 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.

JavaScript
// 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 these

Floor plan and measurements

Cut a plan at a named storey, then let the reader measure — and read the results back in SI units.

JavaScript
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.

JavaScript
// 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.

JavaScript
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.

JavaScript
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().

JavaScript
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.

JavaScript
// <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.

JavaScript
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.

JavaScript
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.

JavaScript
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.

JavaScript
// 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:

WhengetValidation() returns
Before any validationnull — getValidationStatus() says idle.
While the first run is goingnull — 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 goingThe previous numbers, with status: 'running'.
After a run failedThe 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.

HTML
<script type="module">
  import { IfcViewer } from "https://www.ifcvieweronline.eu/sdk/1.18.0/ifc-viewer.es.js";
</script>
PathCachingUse
/sdk/ifc-viewer.es.jsrevalidated on every loadTrying things out — always the latest.
/sdk/1.18.0/ifc-viewer.es.jsimmutable, cached for a yearProduction — 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:

HTML
<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.