WC Canvas

WebComponents, from first element to production system.

This guide covers the Web Platform APIs first, then the WC canvas framework. Standard Web Components are DOM-based; WC adds an optional canvas renderer while preserving HTML fallbacks and browser escape hatches.

1. The mental model

A Web Component is a browser-native custom HTML element. It is not a framework, a template language, or automatically a Shadow DOM component. The platform is made from four independent APIs: custom elements, Shadow DOM, HTML templates, and standard DOM events.

Use custom elements when you want reusable behavior with a stable HTML interface. Keep the public contract small: attributes for declarative configuration, properties for rich values, events for outputs, and slots for content owned by the consumer.

2. Custom elements

Autonomous elements

class UserBadge extends HTMLElement {
  connectedCallback() {
    this.textContent = `User: ${this.getAttribute("name") ?? "Anonymous"}`;
  }
}
customElements.define("user-badge", UserBadge);

Names must contain a hyphen. Definitions are global and can only be registered once. Always guard registration in shared bundles or use a unique package prefix.

Customized built-ins

class FancyButton extends HTMLButtonElement with { extends: "button" } exists, but support and framework integration are less predictable. Prefer autonomous elements for portable components.

Upgrade timing

Unknown tags are still valid DOM. They upgrade when their definition is registered. Use customElements.whenDefined("user-badge") when code must wait for behavior.

3. Lifecycle

CallbackUse
constructor()Initialize state. Do not read children or attributes that may not exist yet.
connectedCallback()Attach listeners, render, and start observers.
disconnectedCallback()Remove listeners, abort fetches, stop timers and observers.
adoptedCallback()React when moved between documents.
attributeChangedCallback(name, old, value)React to declared observed attributes.
static observedAttributes = ["open"];
attributeChangedCallback(name, oldValue, newValue) {
  if (oldValue !== newValue) this.render();
}

4. Attributes, properties, and state

Attributes are strings and are ideal for serialized, declarative values. Properties can hold objects, arrays, functions, or signals. Do not reflect every property automatically: define one source of truth to avoid loops.

element.setAttribute("count", "3"); // declarative string
element.items = [{ id: 1 }];          // rich property
element.addEventListener("change", handler); // output

For booleans, presence means true: <my-dialog open>. For numbers and JSON, validate input and provide safe defaults. Never evaluate attribute strings as code.

5. Shadow DOM

const root = this.attachShadow({ mode: "open" });
root.innerHTML = `<style>:host { display:block }</style>
  <button part="trigger"><slot>Open</slot></button>`;

open exposes shadowRoot; closed hides the reference but is not a security boundary. Shadow DOM scopes styles and changes event retargeting. It does not isolate all inherited values, and it does not automatically make content accessible.

Use :host, :host-context() carefully, CSS custom properties for theming, and ::part() for intentionally public styling hooks. Avoid leaking internal class names as API.

6. Templates and slots

<template> stores inert markup in a DocumentFragment. A named slot lets the component consumer provide content while the component controls layout.

<my-card>
  <span slot="title">Account</span>
  Body content
</my-card>

<slot name="title">Untitled</slot>
<slot>Empty</slot>

Use slotchange when assigned content changes. Query slotted nodes with slot.assignedElements(), not from inside the shadow root with ordinary selectors.

7. Events and communication

Dispatch public events with a stable detail object. Use bubbles: true for parent delegation and composed: true when the event must cross a shadow boundary.

this.dispatchEvent(new CustomEvent("user-change", {
  detail: { id: this.userId }, bubbles: true, composed: true
}));

Events are not state synchronization. Keep events as notifications and let consumers read the current property. Use AbortController to clean up listeners reliably.

8. Forms and native controls

Use real inputs, buttons, and links whenever their browser behavior is valuable. A form-associated custom element can participate in validation and submission:

class ColorField extends HTMLElement {
  static formAssociated = true;
  internals = this.attachInternals();
  set value(value) { this._value = value; this.internals.setFormValue(value); }
  get value() { return this._value; }
}
customElements.define("color-field", ColorField);

Implement labels, name, disabled, validity, reset, and restore-state behavior before replacing a native control.

9. Accessibility

Start with semantic elements and native controls. Give every interactive component a name, visible focus indicator, keyboard operation, and sensible disabled state. Do not use role="application" to hide poor semantics.

10. Starting with WC

<canvas id="app"></canvas>
<script type="module">
  import { createApp, h } from "/cdn/wc-web.js";
  const app = createApp(document.querySelector("#app"));
  app.mount(() => h("main", {}, h("h1", {}, "Hello, WC")));
</script>

WC can be better than traditional Web Components in areas such as highly controlled rendering, visual consistency, custom graphics, and avoiding large DOM trees. The visible UI is painted into canvas, while its accessibility mirror and optional DOM bridge preserve browser behavior where needed.

11. Rendering, layout, and components

Use h(type, props, ...children) to create virtual nodes. Use createSignal(value) for small reactive state and call app.update(view) after changing it.

const count = createSignal(0);
const view = () => h("button", {
  label: `Count: ${count()}`,
  onClick: () => { count.set(count() + 1); app.update(view); }
});

Supported layout styles include padding, width, height, wrapping text, and a flex subset: display, flexDirection, gap, flex, and alignItems. Components support background, borderRadius, border, shadow, and depth.

12. Interactions and DOM escape hatches

Links, scrolling, focus, keyboard activation, clipboard callbacks, and screen-reader content work by default. Text drag selection is opt-in because selection styling is a product decision, not a framework identity.

import { dom } from "/cdn/wc-web.js";
const nativeInput = document.querySelector("input");
h("section", {}, dom(nativeInput, "focus"));

The DOM bridge intentionally translates only focus, click, select, and copy. Keep native DOM nodes for file pickers, text editing, media controls, and third-party widgets.

13. Images, fonts, and shaders

import { image, registerImageDecoder } from "/cdn/wc-web.js";
image("/assets/hero.webp", { width: 640, height: 360 });
registerImageDecoder("qoi", async (buffer) => decodeQoi(buffer));

Browser-supported image formats use Image. Truly unsupported formats require a decoder that returns an image-compatible object. Fonts can be loaded with loadFonts([{ family, source, descriptor }]). Shaders use vertex { ... } and fragment { ... } GLSL blocks and are composited from an offscreen WebGL layer.

14. SSR, progressive enhancement, and SEO

WC does not require SSR. If your site already uses SSR, render semantic HTML first, include a canvas beside it, and pass the fallback selector to createApp. WC hides the fallback only after client mount; without JavaScript, crawlers and users still get real HTML.

<main id="seo-fallback">
  <h1>Product dashboard</h1>
  <p>Server-rendered description and links.</p>
</main>
<canvas id="app"></canvas>

Provide a unique title, description, canonical URL, Open Graph metadata, structured data where appropriate, stable URLs, real anchor links, and an XML sitemap. Canvas pixels are not an SEO document.

15. Routing and CDN

Routes belong to the host site, not the renderer. The included _routes maps the landing page, docs, example, and /cdn/wc-web.js. Deploy the repository on a static host or serve locally:

python3 -m http.server 8080
# http://localhost:8080/
# http://localhost:8080/docs/

The CDN entrypoint currently re-exports the framework module. For production, bundle and pin a version, serve it with a JavaScript content type, and configure immutable caching for versioned URLs.

16. Production checklist

17. Reference

APIPurpose
h(type, props, children)Create a virtual node.
createApp(canvas, options)Create, mount, update, unmount, or destroy a canvas app.
createSignal(initial)Small reactive getter with .set() and .subscribe().
image(src, props)Canvas image vnode.
registerImageDecoder(ext, decoder)Decode a browser-unsupported image format.
dom(node, actions)Pass an existing DOM node and limited native actions.
loadFonts(fonts)Load and wait for canvas fonts.
web.jsonFeature defaults and opt-outs generated by the compiler.

Rule of thumb: use Web Platform semantics first, WC canvas rendering second. If a browser already provides the behavior you need, keep the native element.