--- url: /docs/about.md --- # About Webuum **Webuum** is a lightweight JavaScript framework built for real websites — not complex web applications. It embraces **native browser APIs**, keeps things **simple and small**, and avoids the bloat of modern SPA frameworks where it's unnecessary. ## Philosophy Most JS frameworks today are built with applications in mind. But many websites don’t need reactive state management, hydration strategies, or massive client runtimes. Webuum was designed from a different angle: * **Native first** – Uses custom elements and web standards. * **Small footprint** – Only 1kB gzipped. * **Enhances server-rendered HTML** — JavaScript adds interactivity without overhead. * **No build step required** – But plays well with bundlers. * **Minimal API surface** – You only need what the platform doesn’t already do well. ## Why not use React, Vue, or similar? Frameworks like React or Vue are excellent — but they're primarily optimized for building **stateful applications**. When you just want a fast, maintainable, interactive **website**, these tools can become overkill. Even modern meta-frameworks like Astro or Qwik try to address the issue — but the cost of hydration and runtime complexity still adds up. Webuum is for cases where: * HTML is already rendered on the server — like Astro, Laravel, or Rails. * You don't need state management, hydration, or a client runtime. * You only need small enhancements or isolated interactivity. * You care about **PageSpeed** and **performance** Take an example from [@tailwindplus/elements](https://tailwindcss.com/blog/vanilla-js-support-for-tailwind-plus), for basic UIs the web components are a perfect fit. ## The Platform is Enough (Almost) Modern web APIs are powerful. Webuum just fills in the small gaps. Some things are still verbose or cumbersome in plain JS — like working with attributes, events, or DOM refs. Webuum introduces a minimal layer: * `props` – typed attributes via `data-*`. * `parts` – scoped DOM references for **Light DOM** & **Shadow DOM**. * `command` – declarative event bindings via native **Command API**. * `WebuumElement` – an extended base class for Custom Elements. And that's (mostly) it. ## The Power of the Web Platform Modern browsers already provide native components like ``, `
`, **popovers**, **view-transitions**, command and more. These often require no additional JavaScript to work, giving you powerful building blocks right out of the box. Webuum builds on this foundation — adding just the minimal layer needed to enhance interactivity without reinventing the wheel. Take an example from [webuum](https://stackblitz.com/github/webuum/webuum/tree/main/examples/vite) example on StackBlitz. It uses the *holy trinity of components*: **popover**, **tooltip**, and **dialog**. All completely native API's, minimum JavaScript. ## What if I need more? Webuum doesn’t compete with bigger tools — it complements them. When you outgrow the basics, you can gradually layer in: * [**Signals API**](https://github.com/tc39/proposal-signals), a TC39 proposal, which can be polyfilled today. * [**Lit**](https://lit.dev/) or other libraries — if your components get more complex. * [Remix](https://remix.run/blog/remix-jam-2025-recap) upcoming Remix 3 event system might be perfect fit. * Your own small utilities, exactly where you need them. ## When to use Webuum Use Webuum if your answer to most of these is **“yes”**: * Do you server-render HTML? * Do you want native components but hate boilerplate? * Do you prefer **progressive enhancement** over full hydration? * Do you want to keep JS minimal and optional? * Do you care about **performance** and **maintainability**? If you’re building a full SPA, Webuum isn’t for you. But if you’re building fast websites, marketing pages, or hybrid stacks — it might be exactly what you need. ## Why I built Webuum After years of building websites, I realized something: Most frameworks are built for apps — not websites. They assume you're rendering everything on the client, hydrating entire pages, or managing complex state. But in many real-world projects — especially marketing sites, landing pages, hybrid stacks — this just adds unnecessary complexity. I wanted something different: * Native Web Components, without boilerplate * Simple attribute-based APIs for props and DOM targeting * No build step required — but works great with bundlers too * Tiny, fast, transparent — **closer to the platform** Webuum came out of this need. It’s not revolutionary. It’s not trying to replace React or Vue.\ It’s trying to do one thing well: make writing interactive **websites** better, simpler, and lighter. If that resonates with you — **welcome aboard**. --- --- url: /docs/command.md --- # Command Webuum integrates the [Invoker Commands API](https://developer.mozilla.org/en-US/docs/Web/API/Invoker_Commands_API), a new browser standard for declaratively wiring UI actions directly in HTML via ` ``` ```js class LoginForm extends WebuumElement { submitForm(event) { console.log('Logging in…') console.log(event.source) // original event target - You forgot something! ``` ```js customElements.define('x-hint-popover', class extends WebuumElement { togglePopover(event) { // Custom logic before showing console.log('Popover is about to be shown') // Native behavior super.togglePopover() // Custom logic after showing this.setAttribute('data-open', '') } }) ``` ::: #### Notes The method name must match the native command (`showPopover`, `close`, `show`, etc.). * You must call `super.method()` to retain the native behavior, otherwise only your custom logic will run. * Native methods work only on elements that support them – e.g. `showPopover()` on elements with the popover attribute, `close()` on ``, etc. * If your element does not inherit from the native element (e.g. not `` or not using `popover`), the native method call will have no effect. * In these cases, no `--` prefix is needed, since you’re extending a native command. * Native commands do not require JavaScript at all – they work purely declaratively using HTML. So extend them only when needed. ## Passing values with a value attribute Commands can accept values directly from the triggering element using the `value=""` attribute. These values are passed to the handler method in your custom element. ### Example ::: code-group ```html ``` ```js customElements.define('x-counter', class extends WebuumElement { $count = 0 connectedCallback() { this.textContent = this.$count } increment({ source }) { this.$count += source.$value this.textContent = this.$count } }) ``` ::: ### Value types The value passed via `value=""` is automatically typecast into the appropriate JavaScript type. Webuum uses smart parsing based on the value’s format: | Input | JavaScript value | |---------------|------------------------| | `true` | `true` (boolean) | | `false` | `false` (boolean) | | `123` | `123` (number) | | `Infinity` | `Infinity` (number) | | `Hello world` | `Hello world` (string) | | `[1, 2]` | `[1, 2]` (array) | | `{ "a": 1 }` | `{ a: 1 }` (object) | This makes passing structured data easy and predictable. #### Notes * The typecast value is available under `event.source.$value` — the raw string stays in `event.source.value` * Arrays and Objects must be valid JSON * You can still access the triggering element via `event.source` * For working with additional values or element state, consider using the [Props](/docs/props) feature. ## When to use addEventListener instead of command The command attribute only works on native ` ``` ::: The `connectedCallback()` runs when the element is added to the page — and `disconnectedCallback()` runs when it’s removed. You can also use the other [lifecycle methods](https://developer.mozilla.org/en-US/docs/Web/API/Web_components/Using_custom_elements#custom_element_lifecycle_callbacks) provided by Custom Elements, like `attributeChangedCallback()` if needed. ## Cleanup with `$signal` `WebuumElement` exposes a lifecycle-bound `$signal` — an [`AbortSignal`](https://developer.mozilla.org/en-US/docs/Web/API/AbortSignal) you can pass to any abortable API, such as `addEventListener` or `fetch`. When the element is disconnected, Webuum aborts the signal for you, so those listeners are removed automatically — no manual teardown needed. This is especially handy for listeners on global targets like `window` or `document`, which would otherwise leak if they aren't removed on disconnect. ```js import { WebuumElement } from 'webuum' customElements.define('x-scroll-spy', class extends WebuumElement { connectedCallback() { // Automatically removed when the element disconnects window.addEventListener('scroll', () => { console.log('scrolling', window.scrollY) }, { signal: this.$signal }) } }) ``` Under the hood, `$signal` is backed by a lazily-created `$controller` ([`AbortController`](https://developer.mozilla.org/en-US/docs/Web/API/AbortController)), which is aborted in `disconnectedCallback`. If the element is reconnected and you read `$signal` again, a fresh signal is created — so the same component keeps working after being moved in the DOM. ::: warning If you override `disconnectedCallback`, call `super.disconnectedCallback()` to keep the automatic cleanup — otherwise the controller won't be aborted. ```js disconnectedCallback() { super.disconnectedCallback() // your own cleanup } ``` ::: When using [Customized Built-in Elements](#customized-built-in-elements) or the [Composition Definition](#composition-definition) approach, call `defineSignal()` to enable `$signal` and abort `this.$controller` yourself in `disconnectedCallback`. ## Customized Built-in Elements The Web Components spec allows you not only to define new elements — but also to extend **built-in HTML elements** like ``, ` ``` ::: This lets you keep native behaviors like the built-in modal behavior — while still customizing it. > Note: Safari does not support this feature and doesn’t plan to. You can use a lightweight polyfill though, learn more on [Polyfills](/docs/polyfills) page. ### Why this matters By enhancing native elements instead of replacing them: * You retain accessibility and behavior by default. * You can use browser features like `showModal()`, or native form controls. * You avoid reimplementing things that the browser already does well. ## Composition Definition If you don't want to extend `WebuumElement`, you can manually enable just the features you need — like commands, parts, or props — on any element. This gives you **fine-grained control** and lets you **minimize runtime size** even further. Use this approach when: * You only need **one or two features** from Webuum. * You want to **extend a built-in element** without using `WebuumElement`. * You prefer full control over how and when things are initialized. ### Example ::: code-group ```ts import { defineCommand, defineParts, defineObserver, defineProps, defineSignal } from 'webuum' customElements.define('x-hello-world', class extends HTMLDivElement { // Optional: declared fields for editor autocomplete declare $foo: HTMLElement | null declare $fuu: HTMLElement | null declare $buu: string declare $parts: object declare $shadowParts: object constructor() { super() // Enable command attribute support defineCommand(this) // Map parts from light DOM this.$parts = defineParts(this, { $foo: null, }) // Map parts from shadow DOM this.$shadowParts = defineParts(this.shadowRoot, { $fuu: null, }) // Declare props bound to data attributes defineProps(this, { $buu: null, }) // Observe commands and parts dynamically (light + shadow DOM) defineObserver(this, this.$parts) defineObserver(this.shadowRoot, this.$shadowParts) // Optional: enable the lifecycle-bound `$signal` defineSignal(this) } disconnectedCallback() { this.$controller?.abort() } }, { extends: 'div' }) ``` ```html
Light DOM part
``` ::: This gives you the same feature set as `WebuumElement` — but only what you actually use. You can even skip parts you don’t need: for example, use `defineProps()` only if that’s all you care about. --- --- url: /docs/elements.md --- # Elements Webuum ships ready-made element mixins that package common patterns built on top of its helpers. Like the [observers](/docs/observers), they live in a separate entry point, so you only pay for them when you actually use them: ```js import { WebuumLazyElement } from 'webuum/elements' ``` ## `WebuumLazyElement` `WebuumLazyElement(Element)` is a class mixin — it takes any custom element constructor and returns a subclass wired for lazy initialization, deferring expensive setup until the element scrolls into view. Put your deferred initialization into a `lazyCallback()` method and control it with the `$lazy` [prop](/docs/props): * when `$lazy` is truthy, `lazyCallback()` waits until the element intersects the viewport, then runs once — the mixin resets `$lazy` to `false` afterwards, so it won't fire again on later intersections * when `$lazy` is falsy, `lazyCallback()` runs right away on `connectedCallback` — lazy loading becomes opt-in (or opt-out) per instance via the `data-lazy` attribute Under the hood the mixin runs [`defineElement()`](/docs/element#composition-definition) in the constructor, so Webuum features like [props](/docs/props) and the lifecycle-bound [`$signal`](/docs/element#cleanup-with-signal) work even on base classes that don't extend `WebuumElement`. On connect it starts a [`defineIntersectionObserver`](/docs/observers#defineintersectionobserver) (with `threshold: 0.1`), and on disconnect it aborts `$controller`, which also disconnects the observer. ```js import { WebuumLazyElement } from 'webuum/elements' customElements.define('x-comments', class extends WebuumLazyElement(HTMLElement) { static props = { $lazy: true, } lazyCallback() { // expensive work — fetch data, import a script, hydrate… } }) ``` ```html ``` It also works with [Customized Built-in Elements](/docs/element#customized-built-in-elements) — for example deferring a form's setup, including listeners bound to `$signal`: ```js import { WebuumLazyElement } from 'webuum/elements' customElements.define('x-form', class extends WebuumLazyElement(HTMLFormElement) { static props = { $lazy: true, } lazyCallback() { this.noValidate = true this.addEventListener('submit', this.validateForm, { signal: this.$signal }) } }, { extends: 'form' }) ``` ::: tip If you need different `IntersectionObserver` options, or want to react to every intersection change instead of a one-shot initialization, use [`defineIntersectionObserver`](/docs/observers#defineintersectionobserver) with your own `intersectCallback(entry)` directly — see the [lazy load example](/docs/observers#example-lazy-load-an-element). ::: --- --- url: /docs.md --- # Getting Started You can use Webuum with or without a bundler — it's designed to be as native and lightweight as possible. ::: warning This is a pre-release version. Use in production with caution — the API's may still change before the stable release. ::: ::: tip Migrating from Stimulus? Follow the [Stimulus to Webuum migration guide](/docs/migrations/stimulus) to move controllers, targets, values and actions to Custom Elements and native browser APIs. ::: ## Installation ### Via package manager ::: code-group ```bash [npm] npm install webuum ``` ```bash [Yarn] yarn add webuum ``` ```bash [pnpm] pnpm add webuum ``` ```bash [Bun] bun install webuum ``` ```bash [Deno] deno install npm:webuum ``` ::: Then import it in your JavaScript: ```js import { WebuumElement } from 'webuum' ``` ### Via CDN ```js import { WebuumElement } from 'https://cdn.jsdelivr.net/npm/webuum/dist/index.js' ``` ### Via CDN with importmap ::: code-group ```js [example.js] import { WebuumElement } from 'webuum' ``` ```html [example.html] ``` ::: ## Hello World Example A minimal custom element using Webuum. ::: code-group ```js [example.js] import { WebuumElement } from 'webuum' customElements.define('x-hello-world', class extends WebuumElement { static parts = { $foo: 'custom-name', // maps to data-x-hello-world-part="custom-name" } static props = { $buu: null, // maps to data-buu="Hello world" } connectedCallback() { this.$foo.textContent = this.$buu } }) ``` ```ts [example.ts] import { WebuumElement } from 'webuum' customElements.define('x-hello-world', class extends WebuumElement { declare $foo: HTMLElement | null declare $buu: string | null static parts = { $foo: 'custom-name', // maps to data-x-hello-world-part="custom-name" } static props = { $buu: null, // maps to data-buu="Hello world" } connectedCallback() { this.$foo.textContent = this.$buu } }) ``` ```html [example.html] ``` ::: You’ve just created your first Webuum component — built on native APIs, ready for production, and weighing less than a kilobyte. ## Trying Webuum Online On [StackBlitz](https://stackblitz.com/) or [GitHub](https://github.com/webuum/webuum/tree/main/examples) with basic examples how to use it with [Vite](https://vitejs.dev/) or other frameworks. * vanilla * vite * astro * astro extended --- --- url: /docs/migrations/stimulus.md description: >- Migration guide for replacing Stimulus controllers with Webuum Custom Elements and native browser APIs. --- # Migrate from Stimulus to Webuum Stimulus and Webuum both progressively enhance server-rendered HTML, but they organize behavior differently. Stimulus attaches controller instances to existing elements through an application runtime. Webuum uses native Custom Elements, browser lifecycle callbacks, DOM events and a small set of helpers for commands, parts and props. This is therefore not a dependency-only upgrade. The goal is to move each controller's behavior to the platform or to a focused Custom Element, then remove the Stimulus application and dependency completely. ::: warning Webuum is currently pre-release This guide targets Webuum `0.2.x`. Review Webuum's changelog before upgrading later pre-release versions because APIs may still change before the stable release. ::: ## Decide what should replace each controller Do not recreate every Stimulus controller automatically. Audit what the controller does first: | Existing controller responsibility | Recommended replacement | | --- | --- | | Opens a dialog or popover, toggles details, or duplicates another browser API | Use the native HTML API and remove the controller | | Owns local behavior for a reusable piece of markup | Create an autonomous or customized built-in Custom Element | | Only coordinates a parent with its descendants | Use one Custom Element with Webuum parts and delegated listeners | | Invokes an action on another component | Use native `command` and `commandfor` attributes | | Notifies a parent or unrelated application code | Dispatch a bubbling `CustomEvent` | | Manages application-wide state, client routing or a complex SPA | Keep the appropriate application architecture; Webuum is not a state-management or SPA framework | The migration should reduce abstraction where the platform already provides the behavior. Avoid building a new global controller registry on top of Webuum. ## 1. Install Webuum alongside Stimulus Keep Stimulus installed while migrating incrementally: ```shell npm install webuum ``` Define the first migrated element in an application entry point: ```js import { WebuumElement } from 'webuum' class FilterElement extends WebuumElement { connectedCallback() { // Component setup } } customElements.define('x-filter', FilterElement) ``` Custom-element names must contain a hyphen. A definition runs once per name and automatically upgrades matching elements that already exist or are inserted later; there is no equivalent of `Application.start()`. Stimulus and Webuum can coexist during the migration. Remove the Stimulus controller from a piece of markup as soon as its Custom Element takes ownership, so the same interaction is never connected twice. ## 2. Choose the element root Stimulus can attach a controller to any existing element. With Webuum, that element becomes the component boundary. ### Autonomous Custom Elements Use an autonomous Custom Element for a component without a native element that already expresses its semantics: ::: code-group ```html [Stimulus]
``` ```html [Webuum] ``` ::: Autonomous elements extend `WebuumElement` and use a tag such as ``. They are inline by default, so preserve the old root's `display`, sizing and other layout styles explicitly when changing from a block-level element such as `
`. ### Customized built-in elements When the Stimulus controller enhances a native element such as ``, ` ``` ```html [Webuum] ``` ```js [Webuum handler] clear({ source }) { console.log(source.$value) // true } ``` ::: For an external button, give the Custom Element an ID and add `commandfor`: ```html ``` A command's `value` is available as a typecast `event.source.$value`; the original string remains in `event.source.value`. For several action params, read named `data-*` attributes from `event.source.dataset`, or keep configuration as props on the target element. ### Other events Commands intentionally handle button actions only. Replace every other action with a native listener: ::: code-group ```html [Stimulus] ``` ```js [Webuum] connectedCallback() { this.addEventListener('input', event => this.filter(event), { signal: this.$signal, }) this.addEventListener('keydown', (event) => { if (event.key !== 'Escape') return event.preventDefault() this.clear() }, { signal: this.$signal }) } ``` ::: Delegate bubbling events from the Custom Element root when possible. This keeps listeners working for descendants inserted later and avoids reconnecting the same listener for every matching part. ## 7. Replace dispatch, outlets and cross-controller calls Webuum does not wrap the platform's event system. Replace `this.dispatch()` with a bubbling `CustomEvent`: ::: code-group ```js [Stimulus] this.dispatch('changed', { detail: { query: this.queryValue }, }) ``` ```js [Webuum] this.dispatchEvent(new CustomEvent('filter:changed', { bubbles: true, detail: { query: this.$query }, })) ``` ::: Choose the least coupled replacement for Stimulus outlets and `getControllerForElementAndIdentifier()`: * Use a part when the other element is an owned descendant of the component. * Use `commandfor` when a button invokes a public method on another element. * Dispatch a bubbling custom event when a child reports a state change to a parent or application code. * Use an explicit DOM reference when two elements genuinely need a direct relationship. Avoid reaching into another Custom Element's internal parts. Treat commands, native methods, props and custom events as its public API. ## 8. Replace Stimulus classes and controller state Stimulus CSS classes do not have a dedicated Webuum equivalent. Use ordinary `classList`, ARIA state and `data-*` attributes. If a class name must remain configurable from HTML, expose it as a prop: ::: code-group ```js [Stimulus] static classes = ['loading'] start() { this.element.classList.add(...this.loadingClasses) } ``` ```html [Stimulus markup]
``` ```js [Webuum] static props = { $loadingClass: 'is-loading', } start() { this.classList.add(...this.$loadingClass.split(' ')) } ``` ```html [Webuum markup] ``` ::: Keep private runtime state as fields on the Custom Element. Reflect state to an ARIA attribute, native property or `data-*` attribute when CSS, server-rendered markup or external code needs to observe it. ## 9. Complete example This example combines lifecycle, targets, values and actions in one migration. ::: code-group ```js [filter_controller.js] import { Controller } from '@hotwired/stimulus' export default class extends Controller { static targets = ['input', 'item', 'empty'] static values = { query: { type: String, default: '' }, } connect() { this.update() } filter({ target }) { this.queryValue = target.value this.update() } clear() { this.inputTarget.value = '' this.queryValue = '' this.update() } update() { const query = this.queryValue.toLowerCase() let visible = 0 this.itemTargets.forEach((item) => { const match = item.textContent.toLowerCase().includes(query) item.hidden = !match if (match) visible++ }) this.emptyTarget.hidden = visible > 0 } } ``` ```html [Stimulus markup]
  • News
  • Guides
``` ```js [FilterElement.js] import { WebuumElement } from 'webuum' export class FilterElement extends WebuumElement { static parts = { $input: null, $item: null, $empty: null, } static props = { $query: '', } connectedCallback() { this.addEventListener('input', ({ target }) => { if (target !== this.$input) return this.$query = target.value this.update() }, { signal: this.$signal }) this.update() } clear() { this.$input.value = '' this.$query = '' this.update() } update() { const query = String(this.$query).toLowerCase() const items = [this.$item].flat().filter(Boolean) let visible = 0 items.forEach((item) => { const match = item.textContent.toLowerCase().includes(query) item.hidden = !match if (match) visible++ }) if (this.$empty) this.$empty.hidden = visible > 0 } } customElements.define('x-filter', FilterElement) ``` ```html [Webuum markup]
  • News
  • Guides
``` ::: The migrated element owns the same local behavior without a controller application, action parser or manual reconnect cleanup. ## 10. Migrate incrementally and remove Stimulus Use a controller-by-controller rollout: 1. Inventory every registered controller and its templates, targets, values, actions, classes, outlets and external calls. 2. Remove controllers whose behavior is now native HTML. 3. Choose one local controller and define its Custom Element. 4. Replace its root, targets, values and actions in every template. 5. Remove that controller's registration and import in the same change. 6. Test initial server-rendered markup, dynamically inserted markup, disconnect and reconnect behavior. 7. Repeat until only application-specific controllers remain. 8. Migrate those controllers using this guide, unless they are outside Webuum's website-focused scope. Search for remaining Stimulus dependencies and descriptors: ```text @hotwired/stimulus Application.start application.register data-controller data-action data-*-target data-*-value data-*-class data-*-outlet data-*-param ``` Once no application code or third-party integration requires Stimulus, remove its bootstrap and dependency: ```shell npm uninstall @hotwired/stimulus ``` Check the lockfile and production bundle to confirm that Stimulus is no longer included transitively. ## Polyfills Webuum uses native Invoker Commands, and customized built-ins need a fallback in Safari. Install only the fallbacks required by the project's browser support matrix: ```shell npm install invokers-polyfill @webreflection/custom-elements-builtin ``` You can load Webuum's conditional polyfill entry before defining elements: ```js import 'webuum/polyfill' ``` Or use the individual feature checks from the [Polyfills documentation](/docs/polyfills) when the application needs more control. ## Final checklist * Every migrated Custom Element is defined exactly once. * Customized built-ins keep their native element, `is` attribute and registration option. * Existing block-level roots keep the intended layout after becoming autonomous elements. * Button actions use commands; input, keyboard, pointer and global events use listeners with `$signal`. * Native dialog, popover and details behavior is not reimplemented unnecessarily. * Dynamic parts and server-inserted Custom Elements initialize correctly. * Overridden `disconnectedCallback()` methods preserve Webuum cleanup. * No component is initialized by both Stimulus and Webuum. * Stimulus imports, registration, descriptors and production bundle code are gone. --- --- url: /docs/observers.md --- # Observers Webuum ships a small set of observer helpers that connect native browser observers to your element's lifecycle. They live in a separate entry point, so you only pay for them when you actually use them: ```js import { defineIntersectionObserver } from 'webuum/observers' ``` ## `defineIntersectionObserver` `defineIntersectionObserver(host, options?)` observes the `host` element with an [`IntersectionObserver`](https://developer.mozilla.org/en-US/docs/Web/API/IntersectionObserver) and forwards each change to an `intersectCallback(entry)` method on the host. Branch on `entry.isIntersecting` to react to the element entering or leaving the viewport. It accepts the standard [`IntersectionObserverInit`](https://developer.mozilla.org/en-US/docs/Web/API/IntersectionObserver/IntersectionObserver#options) options (`root`, `rootMargin`, `threshold`) and returns the created `IntersectionObserver`, so you can `disconnect()` it yourself when you're done. The observer is also wired to the element's lifecycle: when [`$signal`](/docs/element#cleanup-with-signal) aborts on disconnect, it disconnects automatically — no manual teardown needed. ```js import { WebuumElement } from 'webuum' import { defineIntersectionObserver } from 'webuum/observers' customElements.define('x-reveal', class extends WebuumElement { connectedCallback() { defineIntersectionObserver(this, { threshold: 0.1 }) } intersectCallback(entry) { if (entry.isIntersecting) { this.classList.add('is-visible') } } }) ``` ### Example: lazy load an element A common use case is to defer the initialization of a heavier element until it scrolls into view. Keep a reference to the returned observer so you can `disconnect()` it once it has done its job, then run the original lifecycle: ```js import { Form } from 'winduum-elements/components/form/index.js' import { defineIntersectionObserver } from 'webuum/observers' customElements.define('x-form', class Element extends Form { static props = { $lazy: false, } connectedCallback() { if (!this.$lazy) return super.connectedCallback() this.$observer = defineIntersectionObserver(this, { threshold: 0.1 }) } intersectCallback(entry) { if (entry.isIntersecting) { this.$observer.disconnect() super.connectedCallback() } } }, { extends: 'form' }) ``` With `data-lazy` set, `connectedCallback()` skips its normal setup and instead starts observing. As soon as the element intersects the viewport, the observer is disconnected and `super.connectedCallback()` runs the real initialization. ::: tip When extending a [Customized Built-in Element](/docs/element#customized-built-in-elements) like the `
` above, make sure `$signal` is available (via `WebuumElement`, `defineElement()`, or `defineSignal()`) if you want the observer to disconnect automatically on element removal. ::: Webuum also ships this pattern pre-packaged as the [`WebuumLazyElement`](/docs/elements#webuumlazyelement) mixin. --- --- url: /docs/parts.md --- # Parts Working with DOM nodes inside a custom element can get messy — especially when you need to repeatedly query selectors with `querySelector` or `getElementById`. Webuum introduces a Parts system that makes this much simpler and more maintainable. Parts are essentially named references to DOM elements inside your component. They work similarly to `ref` in Vue, but are based entirely on web standards. ## Defining Parts Parts are declared in the `static parts` map of your component. Each key is prefixed with a `$` for unambiguous reference in your component, you can also use a custom prefix if you like. By default, the key (e.g., `$foo`) is used as the part name in the DOM. But you can also map it to a custom name. ```js static parts = { $foo: null, // expects part="foo" (shadow DOM) or data-x-hello-world-part="foo" (light DOM) $bar: 'hello', // maps $bar to part="hello" / data-x-hello-world-part="hello" } ``` This makes it possible to keep JavaScript variable names short and consistent while still using semantic names in your HTML. ::: code-group ```html [Light DOM]
``` ```html [Shadow DOM]
``` ::: ```js this.$foo // → HTMLElement this.$bar // → HTMLElement ``` ### Multiple matches If there are multiple elements with the same part name, Webuum will return an array of elements instead of a single reference. ::: code-group ```html [Light DOM]
``` ```html [Shadow DOM]
``` ::: ```js this.$foo // → [HTMLElement, HTMLElement] ``` ### Multiple names A single element can expose multiple part names: ::: code-group ```html [Light DOM]
``` ```html [Shadow DOM]
``` ::: In this case, the element will be available under each matching key in your component. ## Light DOM In the light DOM (outside shadow roots), parts are defined using a `data-[element-name]-part` attribute. The prefix ensures that parts are scoped to the right custom element and avoids collisions with other components. For example, for a custom element ``, you can declare a part like this: ```html
Hello!
``` In your component, you declare the part in the static parts map: ::: code-group ```js import { WebuumElement } from 'webuum' customElements.define('x-hello-world', class extends WebuumElement { static parts = { $foo: null, } connectedCallback() { console.log(this.$foo) //
} } ) ``` ```ts import { WebuumElement } from 'webuum' customElements.define('x-hello-world', class extends WebuumElement { declare $foo: HTMLElement | null static parts = { $foo: null, } connectedCallback() { console.log(this.$foo) //
} } ) ``` ::: This way, `$foo` is automatically bound to the `
` without writing any query selectors manually. ## Shadow DOM In the shadow DOM, parts are declared using the standard `part` attribute — without the element-name prefix, since the shadow root is already scoped to your component. ```html ``` The `static parts` map covers the light DOM of the host element. To bind parts inside a shadow root, call `defineParts` (and `defineObserver` if you want the part callbacks) on the shadow root: ```js import { WebuumElement, defineParts, defineObserver } from 'webuum' customElements.define('x-hello-world', class extends WebuumElement { constructor() { super() const shadowParts = defineParts(this.shadowRoot, { $foo: null, }) defineObserver(this.shadowRoot, shadowParts) } } ) ``` ## Part Callbacks Sometimes you need to run logic when a part becomes available (inserted into the DOM) or when it gets removed. Webuum provides `partConnectedCallback` and `partDisconnectedCallback` for that. Both callbacks receive the part name as the first argument — the key from the `static parts` map, including the `$` prefix — and the element reference as the second argument. ::: code-group ```js import { WebuumElement } from 'webuum' customElements.define('x-hello-world', class extends WebuumElement { static parts = { $foo: null, } partConnectedCallback(name, element) { if (name === '$foo') { console.log('foo connected', element) } } partDisconnectedCallback(name, element) { if (name === '$foo') { console.log('foo disconnected', element) } } } ) ``` ```ts import { WebuumElement } from 'webuum' customElements.define('x-hello-world', class extends WebuumElement { declare $foo: HTMLElement | null static parts = { $foo: null, } partConnectedCallback(name: string, element: HTMLElement) { if (name === '$foo') { console.log('foo connected', element) } } partDisconnectedCallback(name: string, element: HTMLElement) { if (name === '$foo') { console.log('foo disconnected', element) } } } ) ``` ::: When `
` is added, `partConnectedCallback` fires — and when it is removed, `partDisconnectedCallback` fires. `partConnectedCallback` also fires for parts that are already present in the DOM when the component initializes. --- --- url: /docs/polyfills.md --- # Polyfills Webuum builds on modern web APIs — some of which are still experimental or not universally supported. To keep the runtime small, **polyfills are opt-in and can be lazy-loaded** based on feature detection. ## Command API Webuum supports the [Invoker Commands API](https://developer.mozilla.org/en-US/docs/Web/API/Invoker_Commands_API) which introduces a native way to declaratively bind element actions (including custom actions) using a command attribute — without custom event listeners or JavaScript wiring. ```html Dialog Content ``` Since December 2025 this API is [Baseline Newly available](https://developer.mozilla.org/en-US/docs/Web/API/Invoker_Commands_API) — it works in the latest versions of all major browsers. To support older browser versions, you can use [`invokers-polyfill`](https://www.npmjs.com/package/invokers-polyfill). ```shell npm install invokers-polyfill ``` Then lazy-load it when needed using Webuum’s built-in feature detection: ```js import { supportsCommand } from 'webuum/supports' if (!supportsCommand) { const { apply } = await import('invokers-polyfill/fn') apply() } ``` For a deeper explanation of how the Commands API works and how to use it in Webuum, see the [Command](/docs/command) page. ## Customized Built-in Elements Customized built-ins allow you to extend native HTML elements like ``, `` or `