A custom element to
define custom elements
Declarative Custom Elements concept 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>
Props
Declare types inline — count:number="0". Auto-detected when untyped. Changes reflect back to attributes.
Styles
Scoped automatically. Light DOM uses CSS nesting, shadow DOM isolates fully. :host works in both.
Script
this is the element. Runs once per instance. Connect, disconnect, prop change callbacks.
Template
Plug in any template engine — sprae, Alpine, petite-vue, template-parts — or wire DOM yourself.
Prop types
Attributes are strings, so a type suffix like :number coerces the value for you. Skip the suffix and the type is guessed from the value. 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>
Scoped styles
<style> is scoped automatically. Without shadow DOM, :host rewrites to the tag name and styles nest under it. With shadow DOM, styles are fully isolated and shared across instances.
Lifecycle events
<script> runs once per instance at creation. this is the element. onconnected and ondisconnected fire on attach/detach; onpropchange fires when a property changes.
<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>
Template engine
Use any framework instead of wiring DOM manually. Set DE.processor to configure any template engine — reactive bindings replace querySelector boilerplate.
Why this exists
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.
