Macros
Macros are build-time functions that are replaced with generated code during compilation. They eliminate boilerplate common to every web component. Each macro is available globally and requires no import.
$inline
Inlines the contents of a file as a string at build time. The call is replaced with the literal file contents, so HTML and CSS can live in their own files with full editor tooling, linting, and minification, while the compiled output is a plain string.
template.innerHTML = $inline("./my-component.html");The path is resolved relative to the current module.
See Limitations → Writing HTML and CSS as Strings for the problem this addresses.
$upgrade
Handles own properties that were assigned to an element before its custom element class was upgraded.
Call it at the beginning of connectedCallback. During compilation it is replaced with a private field declaration and a loop that deletes each pre-upgrade own property and re-assigns it, so the prototype setters run as if the values had been set after definition.
class MyComponent extends HTMLElement { connectedCallback() { $upgrade(); }}Compiles to:
class MyComponent extends HTMLElement { // Injected as the first field to capture all pre-upgrade values. #__UpgradeProperty = Object.entries(this) as [keyof this, this[keyof this]][];
connectedCallback() { for (const [property, value] of this.#__UpgradeProperty) { delete this[property]; this[property] = value; }
this.#__UpgradeProperty.length = 0; }}The generated field is declared first in the class so it captures all pre-upgrade values before any other field initializer runs. Subsequent calls to connectedCallback — for example when an element is removed and re-attached — are no-ops because the array is cleared after the first run.
See Limitations → Properties Set Before Element Upgrade for the problem this addresses.
$shadow
Creates and initializes a shadow root.
The macro generates the full boilerplate for attaching a shadow root, inlining HTML and CSS, and cloning the template into each instance. The HTML template and CSSStyleSheet are created once as static private fields and reused across all instances of the component class.
Specify either htmlFile or htmlString, and either cssFile or cssString. Using both in the same pair is an error.
All paths are resolved relative to the current module, the same as $inline.
class MyComponent extends HTMLElement { readonly #shadow = $shadow({ htmlFile: "./my-component.html", cssFile: "./my-component.css", mode: "open", });}Compiles to:
class MyComponent extends HTMLElement { static readonly #__Fragment = (() => { const template = document.createElement("template"); template.innerHTML = $inline("./my-component.html"); return template.content; })();
static readonly #__Stylesheet = (() => { const sheet = new CSSStyleSheet(); sheet.replace($inline("./my-component.css")); return sheet; })();
readonly #shadow = (() => { const shadow = this.attachShadow({ mode: "open" }); shadow.adoptedStyleSheets = [MyComponent.#__Stylesheet]; shadow.append(MyComponent.#__Fragment.cloneNode(true)); return shadow; })();}Options
$shadow accepts all ShadowRootInit options, plus the following for specifying HTML and CSS sources:
htmlFile
- Type:
string - Description: Path to an HTML file to inline into the shadow root template.
htmlString
- Type:
string - Description: HTML markup to inline directly.
cssFile
- Type:
string - Description: Path to a CSS file to inline into a constructed stylesheet.
cssString
- Type:
string - Description: CSS source to inline directly.
$defineElement
Registers a component class as a custom element.
Call it as a standalone statement at the end of the module, passing the component class. The tag name is read from the class’s static _componentName, and the generated code skips the definition when that tag name is already registered, so a component bundled more than once on the same page does not throw a NotSupportedError.
class MyComponent extends HTMLElement { static readonly _componentName = "my-component";}
$defineElement(MyComponent);Compiles to:
if (!customElements.get(MyComponent._componentName)) { customElements.define(MyComponent._componentName, MyComponent);}The class must be passed by identifier, not as an expression.
$forwardAria
Forwards ARIA labelling and description from the current element to a child element.
Call it anywhere in a method body, passing the element that should inherit the accessible name and description. During compilation it is replaced with three statements: marking the host as a presentational element, and prepending the host to the child element’s ariaLabelledByElements and ariaDescribedByElements chains.
class MyComponent extends HTMLElement { connectedCallback() { $forwardAria(this.#input); }}Compiles to:
class MyComponent extends HTMLElement { connectedCallback() { this.role = "presentation"; this.#input.ariaLabelledByElements = [this, ...(this.ariaLabelledByElements ?? [])]; this.#input.ariaDescribedByElements = [this, ...(this.ariaDescribedByElements ?? [])]; }}Setting role="presentation" removes the host element from the accessibility tree so that assistive technologies interact directly with the forwarded-to element. Existing entries in either elements-list are preserved by spreading them after the host.
$forwardAria is designed to be called as a standalone statement. If it is used in an expression position, the compiler wraps the generated statements in an IIFE and substitutes that instead.