A custom element to
define custom elements

Declarative Custom Elements conceptual 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

Type suffix like :number coerces the value. No type autoinfers from default. 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: light DOM nests it under the tag name, shadow DOM isolates it fully. :host works in both.

Light DOM Shadow DOM

          
Result

Lifecycle events

<script> runs once per instance; this is the element. onconnected, ondisconnected and onpropchange fire on attach, detach and prop change.

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

Set DE.processor to connect any template engine. Its reactive bindings replace manual querySelector wiring.

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 →