Declarative Custom Elements conceptual implementation.
<define-element> <x-counter count:number="0"> <template> <button id="btn"> Clicks: <span id="n">0</span> </button> </template> <script> this.onconnected = () => { let n = this.querySelector('#n') this.querySelector('#btn').onclick = () => n.textContent = ++this.count } </script> </x-counter> </define-element> <!-- use it --> <x-counter></x-counter> <x-counter count="10"></x-counter>
Type suffix like :number coerces the value. No type autoinfers from default. Primitives reflect back to attributes; array and object don't.
<x-widget count:number="0" label:string="Click me" active:boolean > <script> this.count // 0 (number) this.label // "Click me" (string) this.active // false (boolean) this.count = 5 // reflect to attribute this.getAttribute("count") // "5" </script> </x-widget>
| Type | Coercion | Default |
|---|---|---|
| :string | String(v) |
"" |
| :number | Number(v) |
0 |
| :boolean | true unless "false" |
false |
| :date | new Date(v) |
null |
| :array | JSON.parse(v) |
[] |
| :object | JSON.parse(v) |
{} |
| (none) | auto-detect | as-is |
<style> is scoped automatically: light DOM nests it under the tag name, shadow DOM isolates it fully. :host works in both.
<script> runs once per instance; this is the element. onconnected, ondisconnected and onpropchange fire on attach, detach and prop change.
<define-element> <x-clock time:string> <template> <time id="t"></time> </template> <script> let id // template DOM not ready here const tick = () => this.time = new Date().toLocaleTimeString() // added to DOM, template is ready this.onconnected = () => { t = this.querySelector('#t') tick(); id = setInterval(tick, 1000) } // removed from DOM this.ondisconnected = () => clearInterval(id) // property changed → update DOM this.onpropchange = () => t.textContent = this.time </script> </x-clock> </define-element>
Set DE.processor to connect any template engine. Its reactive bindings replace manual querySelector wiring.
The W3C declarative proposal has stalled for years over template syntax disagreements. The polyfill attempts are mostly dead. So you either write boilerplate or avoid custom elements.
<define-element> fills the gap: include the script and write components as HTML. Pick any template engine or skip it entirely.
It's ~2KB (200 loc). If the W3C proposal ships natively, this becomes unnecessary.