A custom element to
define custom elements

Declarative Custom Elements concept implementation.

<script src="unpkg.com/define-element">
index.html
<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>
Result

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.

definition
<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

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.

Light DOM Shadow DOM

          
Result

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.

clock.html
<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>
Result

Template engine

Use any framework instead of wiring DOM manually. Set DE.processor to configure any template engine — reactive bindings replace querySelector boilerplate.

sprae Alpine petite-vue W3C template-parts

        

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.

Read the docs →