Thanks for your interest in contributing. This guide covers how the repo is laid out, how to run it locally, and a few gotchas that aren't obvious from reading the code.
For general guidelines shared across all That Open Company repos (ask-first policy, conventional commits, JSDoc rules, example code rules), read docs.thatopen.com/contributing first. This file only covers what's specific to engine_ui-components.
@thatopen/ui is a library of web components for building BIM web apps. Everything is a custom element (<bim-button>, <bim-panel>, <bim-table>, ...) built on Lit. Drop-in via HTML, no framework required; works alongside React, Vue, Svelte, or plain JS.
The repo ships two packages:
@thatopen/ui(inpackages/core/) — generic UI primitives. No BIM dependencies. Safe to use in any web app.@thatopen/ui-obc(inpackages/obc/) — BIM-specific components built on top of@thatopen/components(viewports, property panels, classification trees, etc.).
packages/
├── core/src/ # @thatopen/ui
│ ├── core/
│ │ ├── Component/ # Base LitElement subclass with lazy-load support
│ │ ├── Manager/ # Custom-element registration (defineCustomElement)
│ │ ├── types.ts
│ │ └── utils.ts
│ └── components/ # The primitives
│ ├── Button/ # <bim-button>
│ ├── Checkbox/ # <bim-checkbox>
│ ├── ColorInput/ # <bim-color-input>
│ ├── ContextMenu/ # <bim-context-menu>
│ ├── Dropdown/ # <bim-dropdown>
│ ├── Grid/ # <bim-grid> (flexible layout grid)
│ ├── Icon/ # <bim-icon>
│ ├── Input/ # <bim-input>
│ ├── Label/ # <bim-label>
│ ├── NumberInput/ # <bim-number-input>
│ ├── Option/ # <bim-option>
│ ├── Panel/ # <bim-panel>
│ ├── PaperSpace/ # <bim-paper-space>
│ ├── Selector/ # <bim-selector>
│ ├── Slider/ # <bim-slider>
│ ├── Table/ # <bim-table>
│ ├── Tabs/ # <bim-tabs>
│ ├── TextInput/ # <bim-text-input>
│ ├── Toolbar/ # <bim-toolbar>
│ ├── Tooltip/ # <bim-tooltip>
│ ├── Viewport/ # <bim-viewport> (Three.js canvas host)
│ ├── Chart/ + ChartLegend/
│ └── ...
└── obc/src/ # @thatopen/ui-obc (BIM-specific)
├── core/
│ └── Manager/ # Registers obc custom elements (<bim-world>, <bim-sheet-board>, ...)
├── components/
│ ├── buttons/ # BIM-flavored buttons (ModelButton, ...)
│ ├── charts/ # BIM charts (model stats, etc.)
│ ├── forms/ # Attribute editors, property-set forms
│ ├── sections/ # Panel sections wired to @thatopen/components
│ └── tables/ # Element tables, classification trees, relations tables
└── utils/
- Everything is a custom element. Define a
LitElementsubclass, register it viaManager.defineCustomElement("bim-thing", Thing). The tag is the primary contract — users consume components through HTML and attributes, not TS imports. - Tag naming:
bim-*. All core and obc tags share thebim-prefix. Pick names that describe the widget, not the BIM concept (e.g.,<bim-table>is generic; the BIM-specific one is named by what it shows, like<bim-world>). - Core knows nothing about BIM. No imports from
@thatopen/componentsor@thatopen/fragmentsinpackages/core/. Primitives are framework-agnostic and should work in a vanilla HTML page. - Obc is where BIM lands. Any component that wires into a
Componentsinstance, reads fragments data, or talks to a 3D scene goes inpackages/obc/. Obc freely depends on core. - Lit templating everywhere. Components render via Lit's
htmltagged template. Events are nativeCustomEventinstances unless wrapped, so they work with any framework. - Lazy loading for large lists. The base
Componentclass hasIntersectionObserversupport for virtualized rendering when a component renders many children (seeTable,Dropdown). Opt in withuseObserver = true.
git clone https://github.com/ThatOpen/engine_ui-components.git
cd engine_ui-components
yarn install
yarn devyarn dev starts a Vite server that serves every example.ts in the repo. Browse to the printed URL and pick one. Changes to source files hot-reload.
Examples import from the local source, so edits to either package show up immediately without rebuilding.
The only time you need an explicit build:
- Before opening a PR, run
yarn build-librariesto verify both packages build cleanly. This is the same check CI runs. - Testing consumer-side integration (i.e., using the built
dist/from another repo), runyarn build-coreand/oryarn build-obc.
- Open an issue first (ask-first policy, per docs.thatopen.com/contributing).
- Decide core vs obc: does it depend on
@thatopen/componentsor BIM data? If yes, obc. If no, core. - Branch from
main. - Create a folder under the appropriate package (e.g.,
packages/core/src/components/MyWidget/) with:index.ts—LitElementsubclass withstatic styles,render(), and any@propertyreactive propsexample.ts+example.html— doubles as docs and regression test (deployed to the docs site)
- Register the custom element in the parent
Manager/index.ts:Manager.defineCustomElement("bim-my-widget", components.MyWidget);
- Add the tag to the global HTML element map in
Manager/index.tsso TS users get autocomplete:declare global { interface HTMLElementTagNameMap { "bim-my-widget": MyWidget; } }
- Re-export the class from the package's top-level
index.ts. - Update
CHANGELOG.mdunder## Unreleasedif user-visible. yarn build-librariesto verify the build.- Open a PR with a conventional-commits title (
feat:,fix:,feat!:for breaking).
- Reproduce in the relevant
example.ts, or tweak an existing one to exercise the broken path. - Keep the fix minimal. CSS tokens and attribute-driven state changes are easy to over-engineer; prefer the smallest patch.
- PR title:
fix: <one-line description>.
litis a shared dep between both packages. Both vite configs externalizelitandlit/*subpaths so each package imports from the consumer'snode_modules/litrather than inlining its own copy. If you bundle lit, the consumer will get the "Multiple versions of Lit loaded" warning. Don't remove theexternal: (id) => id === "lit" || id.startsWith("lit/")rule.- Obc externalizes
@thatopen/ui. Same reason as above: users shouldn't get two copies of core floating around. - Custom element names are global. Once
customElements.define("bim-foo", ...)runs, the name is locked for the page. Pick names carefully and avoid renaming after release — it's a breaking change that can't be softened with deprecation. - CSS vars are the public theming API. Tokens like
--bim-ui_bg-contrast-60,--bim-button--bgc, etc. are consumed by user stylesheets. Renaming them is a breaking change. - Reactive properties need
@propertydecorators (or thestatic propertiesblock) or Lit won't re-render on change. Mutating a plain class field won't trigger an update. - Don't assume
documentorwindowexist at module load. Components must be importable in SSR contexts (Next.js, Astro) without crashing, even if they only render client-side. Put browser API access insideconnectedCallback()orfirstUpdated(). @thatopen/ui-obcpeer-depends on@thatopen/components. Keep the peer range tight; don't bump unless you actually use new core API.
Read the existing examples under packages/*/src/components/**/example.ts — they cover every public component and are the fastest way to see attribute / slot / event patterns.