npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

define-element

v1.5.2

Published

A custom element to define custom elements

Readme

define-element npm size ci

A custom element to define custom elements.

<script src="https://unpkg.com/define-element"></script>
<define-element>
  <x-greeting name:string="world">
    <template>
      <p id="msg"></p>
    </template>
    <script>
      this.onpropchange = () =>
        this.querySelector('#msg').textContent = `Hello, ${this.name}!`
      this.onconnected = () => this.onpropchange()
    </script>
    <style>:host { font-style: italic }</style>
  </x-greeting>
</define-element>

<x-greeting></x-greeting>
<x-greeting name="Arjuna"></x-greeting>

or $ npm i define-elementimport 'define-element'

Definition

Elements are defined by-example inside <define-element>. Each child becomes a custom element. A definition can contain <template>, <script>, and <style>.

<define-element>
  <my-element greeting:string="hello">
    <template>...</template>
    <style>...</style>
    <script>...</script>
  </my-element>
</define-element>

Multiple definitions supported. After processing, <define-element> removes itself. Without <template>, instance content is preserved as-is.

Props

Attributes are strings. Optional type suffix defines coercion. No suffix auto-detects type from value. Plain values (string, number, boolean, date) reflect back to attributes.

<x-widget count:number="0" label:string="Click me" active:boolean>

| Type | Coercion | Default | |------|----------|---------| | :string | String(v) | "" | | :number | Number(v) | 0 | | :boolean | true unless "false" or null | false | | :date | new Date(v) | null | | :array | JSON.parse(v) | [] | | :object | JSON.parse(v) | {} | | (none) | auto-detect | as-is |

Primitive props reflect to attributes and vice versa. Array/object props are property-only (no attribute reflection). Instance attributes override definition defaults.

Template & Script

<script> runs once per instance at creation. this is the element. <template> is then cloned and rendered (by the processor or literally). Lifecycle callbacks onconnected, ondisconnected, onpropchange fire when the element is attached, removed, or any property changes. No eval; scripts run via element injection.

<define-element>
  <x-clock>
    <template>
      <time id="display"></time>
    </template>
    <script>
      let id, display  // persistent closure scope
      const tick = () => display.textContent = new Date().toLocaleTimeString()
      this.onconnected = () => {
        display = this.querySelector('#display')
        tick()
        id = setInterval(tick, 1000)
      }
      this.ondisconnected = () => clearInterval(id)
    </script>
    <style>:host { font-family: monospace; }</style>
  </x-clock>
</define-element>

| Access | Description | |--------|-------------| | this | The element instance (in <script>) | | host | The element instance (in processor templates) | | this.count | Prop value (getter/setter) | | this.props | Prop values object (source of truth) | | this.onpropchange | Prop changed callback (name, val) | | this.onconnected | DOM ready callback (fires on every connect) | | this.ondisconnected | Disconnected callback | | this.onadopted | Adopted callback |

Script runs once before render — like a class body. onconnected fires after render on every connect — like connectedCallback. Async await is auto-detected.

Style

<style> is scoped automatically. Without shadow DOM — via CSS nesting, :host rewrites to the tag name. With shadow DOM — styles are fully isolated, shared across instances via adoptedStyleSheets.

Shadow DOM & Slots

Add shadowrootmode to the template for encapsulation. Slots work natively:

<define-element>
  <x-dialog open:boolean>
    <template shadowrootmode="open">
      <dialog id="dialog">
        <header><slot name="title">Notice</slot></header>
        <slot></slot>
        <footer><button id="close">Close</button></footer>
      </dialog>
    </template>
    <script>
      let dlg, close
      const sync = () => this.open ? dlg.showModal() : dlg.close()
      this.onpropchange = sync
      this.onconnected = () => {
        dlg = this.shadowRoot.querySelector('#dialog')
        close = this.shadowRoot.querySelector('#close')
        close.onclick = () => this.open = false
        sync()
      }
    </script>
    <style>
      dialog::backdrop { background: rgba(0,0,0,.5); }
      header { font-weight: bold; margin-bottom: .5em; }
      footer { margin-top: 1em; text-align: right; }
    </style>
  </x-dialog>
</define-element>

<x-dialog open>
  <span slot="title">Confirm</span>
  <p>Are you sure?</p>
</x-dialog>

Processor

Without a processor, templates are static HTML — cloned automatically, you wire DOM by hand. Set DE.processor to plug in any template engine; reactive bindings replace querySelector boilerplate.

Note: There is one global processor at a time. Setting DE.processor applies to all <define-element> definitions. If you need different template engines for different components, choose one and use manual DOM wiring for the rest.

processor(root, state) => void
  • root — element (light DOM) or shadowRoot (shadow DOM), empty, with root.template
  • state{ host, ...propValues } snapshot from prop defaults + instance attributes
  • host.onpropchange — set this to receive prop updates: (name, val) => {}

In light DOM, non-prop host attributes (parent directives like :each, v-text, x-bind) are temporarily stripped during processor execution so the processor doesn't process them.

let DE = customElements.get('define-element')

// sprae
import sprae from 'sprae'
DE.processor = (root, state) => {
  root.appendChild(root.template.content.cloneNode(true))
  let s = sprae(root, state)
  state.host.onpropchange = (k, v) => s[k] = v
}
<define-element>
  <x-counter count:number="0">
    <template>
      <button :onclick="count++">
        Count: <span :text="count"></span>
      </button>
    </template>
  </x-counter>
</define-element>

No <script> needed — sprae updates the template automatically when state changes. Other processors:

// @github/template-parts
import { TemplateInstance } from '@github/template-parts'
DE.processor = (root, state) => {
  root.replaceChildren(new TemplateInstance(root.template, state))
}

// petite-vue
import { createApp, reactive } from 'petite-vue'
DE.processor = (root, state) => {
  root.appendChild(root.template.content.cloneNode(true))
  let r = reactive(state)
  createApp(r).mount(root)
  state.host.onpropchange = (k, v) => { r[k] = v }
}

// Alpine.js
import Alpine from 'alpinejs'
DE.processor = (root, state) => {
  root.appendChild(root.template.content.cloneNode(true))
  let r = Alpine.reactive(state)
  Alpine.addScopeToNode(root, r)
  Alpine.initTree(root)
  state.host.onpropchange = (k, v) => { r[k] = v }
}

Frameworks with their own component models (Lit, Vue, Stencil) are better used directly.

Why

The W3C Declarative Custom Elements 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 custom elements as HTML. It doesn't impose a template engine or framework — use your favorite one, or just wire DOM by hand.

This ~200-line implementation demonstrates that the W3C proposal is viable and useful. If it ships natively, this becomes unnecessary.

Alternatives

EPA-WG custom-element · tram-deco · tram-lite · uce-template · Ponys · snuggsi · element-modules · Lit · Stencil · FAST · Catalyst · Atomico · Minze · haunted · W3C DCE Proposal

Detailed comparison →

Limitations

  • Async scripts: await in <script> defers everything after it to a microtask. Assign lifecycle callbacks (onconnected, onpropchange) before the first await, or they won't be set when the element first renders.
  • Closed DSD: Declarative shadow DOM with shadowrootmode="closed" works for programmatically created elements. But if the browser consumes a closed DSD template during HTML parsing (on either definition or instance elements), the shadow root is inaccessible — use open mode instead.
  • Safari is="": Customized built-in elements (<ul is="x-sortable">) are not supported in Safari and never will be.
  • Single processor: DE.processor is global — one template engine at a time. Components that need manual DOM wiring can coexist by using <script> blocks instead of processor directives.

License

Krishnized ISC