custom-element
Copyright (c) 2026 Sasha Firsov https://github.com/sashafirsov
Declarative Custom Element (DCE) is a part of pure Declarative Web Application stack. A proof of concept as a part of
WCCG in Declarative custom elements and Declarative Web Application
discussion. NO-JS The functionality of DCE and its data access does not require programming using JavaScript.
It allows to define custom HTML tag with template filled from slots, attributes and data slice as of now from
local-storage and location.
UI is re-rendered on each data slice change triggered by initialization or DOM event.
GitHub
| Live demo: custom-element
| Try in Sandbox
| tests project
| Chrome devtools pugin
HTTP companion retirement
The standalone http-request.js module and HttpRequestElement export are
retired. Remove that script/import and migrate HTTP templates to cem-elements
with type="text/cem-ml". Declare {http-request @slice=response @url="…"} in
the template; request lifecycle and metadata remain under datadom.slices.response.
Its .data is a native CEM document during rendering. Query it through CEM-QL
nodes or a separate xpath-functions library, replacing JavaScript paths such as
.data.items. JavaScript snapshots contain metadata and data: null.
The HTTP examples and version-picker examples
install the shared runtime and ship the required fixtures and function library.
The reusable version picker uses package,
initialversion, showdate, value and the label slot; migrate legacy
package-name to package, current-version to initialversion, and
show-date to showdate. The old npm-version-to-url fragment is replaced by
the documented URL synchronization composition in the version-picker examples.
The shared runtime is included in the package.
See the loader contract for ownership,
error/reload behavior and the later AST streaming phase.
What is DCE?
DCE provides the next level of abstraction in HTML - native composition. With native implementation which is streaming parser, streaming transformation, multithreading. native assumes the C/Rust compiled code. There is no place for JavaScript except of polyfill and ability to extend DCE, which otherwise has to be native.
The composition assumes the fully functional template and ability to call the template with parameters( custom tag + attributes) .
As the next to HTML abstraction layer - composition, it provides:
- ability to use dependencies as from withing the page as from external file/lib via src attribute and # in URL
- ability to treat external content via content-type like html, SVG, images, video with own template rendering
- provide styles and embedded DCE declarations in own and named(lib) scope, sharing the scoped registry.
After composition the layer of functional component provides
- data layer with access to attributes/payload(+slots), dataset, data bound slice
- means in template to use the data selector for condition/enumeration/text injection into attributes and DOM
- Set of native primitives to support browser APIs declaratively: location,storage, http request which bonded to slice and as result to reactive UI.
- support the data change trigger over events
While DCE is no-JS concept, DCE provides the basic declarative constructs to build most of simple apps. Assuming the extending via custom elements and JS. The evolution goal is to adopt most demanded APIs/construct natively into DCE stack over time.
DCE is compatible with closed/open/named root. Enabling as site-scoped styling and registry as encapsulated anonymous scopes in shadow root.
This project is a POC( Proof of Concept ) targeting to become a base for native DCE implementation polyfill.
use
install
use via CDN
NPM, yarn
Enable IDE support
Live demo 🔗
Interactivity via data slice triggered by events
More on slice concept in slice and events demo page
Templating power
comes from XSLT and XPath. Which is natively implemented in all current browsers, globally tested and well documented.
generates HTML
responsive hex-grid demo
, look into sources for samples of CSS encapsulation and external template use.
Implementation notes
Life cycle
custom-element declaration
- constructor injects payload into XSL template
- creates a class for custom element extending HTMLElement
- registers element by
tagattribute
NOTE: attempt to register custom element with already registered tag name would fail due to w3c standard limitations. The scoped custom element registry is still a proposal.
omitting tag leads to template instantiation
Whether template is inline or given by src attribute, the custom-element would be instantiated inline if no tag
attribute is given.
custom element instance
constructor creates XML with
- root matching the tag
- payload
- dom nodes with
slotattribute stay inside
- dom nodes with
- attributes
- ?dataset
DOM content is replaced with results of instance XML transformation by declaration XSLT.
tag attribute
allows to define the Custom Element tag registered by window.customElements.define().
If omitted, the tag is auto-generated and the content is rendered inline.
See demo for tag attribute use.
src attribute
allows to refer either external template or template within external library by #id hash in URL.
The compatibility adapter follows the CEM-ML resource lifecycle through the
cem-element external resource loading contract, covering src="#id",
src="url", src="url#id", module-map policy, security context, and expected
content-type context passed to CEM-ML.
See demo with various samples.
types of template
- HTML with DCE syntax ( slots, data slices, xslt operators, etc. )
- SVG image, MathML, etc.
- XSLT template. The
datadomis the XML payload for transformation. In order to be embedded into external document, this document has to have XML syntax like XHTML. Attempt of including XSLT within HTML file would break the template integrity by parser.
#id Local reference
allows to refer the template withing same document
url
allows to use the external document as template
url#id
allows to refer the template withing external document
template syntax
Scoped CSS live demo
styles encapsulation
DCE can have the own styles which would be scoped to the instances.
In order to prevent the style leaking, it has to be defined withing template tag:
override style for instance
In same way as in DCE itself:
Attributes
To be served by IDE and to track the attributes changes, they have to be declared via attribute:
The curly braces {} in attributes implemented as attribute value template
The names in curly braces are matching the instance attributes. I.e. in XML node /my-component/attributes/.
To access payload XPath could start with /*/payload/. I.e. {/*/payload//label} refers to all label tags in payload.
Slots
<slot name="xxx"> is replaced by payload top elements with slot attribute matching the name,
i.e. slot xxx is matching <i slot="xxx">...</i> in payload.
loops, variables
Loop implemented via for-each. See for-each table demo for examples including nested loops and XHTML namespace usage.
XPath
is available in {} in attributes, in for-each, if, value-of, and other XSL tags.
XPath is a selector language to navigate over custom element instance data, attributes, and payload.
XSLT 1.0 — engine retired, legacy templates supported via conversion (0.1.0)
How it works now (0.1.0): the in-browser native XSLT 1.0 transform engine is retired —
<custom-element>no longer runs a browser XSLT processor. Instead, legacy HTML+XSLT templates are transpiled to CEM-ML and rendered on the CEM-ML / CEM-QL substrate (CemElementRuntime), the same engine hand-authoredtype="cem-ml; version=0.0"templates use. A legacy sample and its migrated CEM-ML twin therefore render identically, on one engine.Supported legacy constructs (auto-detected; the template DOM is parsed with HTML + XSLT namespaces and converted):
<xsl:value-of>/{…}interpolation,<xsl:for-each>(incl. theexsl:node-set($var)/*inline-variable idiom, unrolled),<xsl:if>/<xsl:choose>/when/otherwise(and the barefor-each/if/choosespellings),<xsl:variable>,<slot>, declaration<attribute>/<slice>, attribute value templates, and the XPath function subset the legacy demos use (contains,not,translate,substring(-before/-after),string-length,position(),count,concat,normalize-space,hasBoolAttribute, comparisons). Bare{name}/{$name}/{//path}interpolation and??coalescing are supported.Not converted (Tier 3, deferred): XSLT outside the bounded compatibility profile, including EXSLT
func:function,<msxsl:script>(embedded JScript — non-transpilable), and dynamic construction names outside the scalar AVT subset. These emit a conversion diagnostic. Author such logic in CEM-ML/CEM-QL directly.The historical XSLT 1.0 reference below documents the original authoring surface; most of it is reproduced by the converter. The original POC used the native XSLT 1.0 processor — that engine dependency is what 0.1.0 retires, not the authoring dialect.
<template lang="custom-element-v0"> remains a deprecated browser compatibility
selector through the next-major migration window. Untyped legacy templates on
<custom-element> are normalized to that selector; both forms lower to canonical
CEM-ML and render through the same cem_ql substrate. New authoring should use
type="text/cem-ml". The selector may be removed only after retained demos,
material fixtures, and downstream generator workflows have canonical CEM-ML
replacements and the FF-5 usage inventory reaches its removal gate.
troubleshooting
HTML parser is not compatible with templates
On many tags like table, or link a the attempt to use XSLT operations could lead to DOM order mismatch to given
in template. In such cases the xhtml: prefix in front of troubled tag would solve the parsing.
See demo source for detailed sample.
Chrome devtools plugin
@epa-wg/custom-element plugin gives the view into
currentselected in DOM inspector nodeParent
customElementDeclarative Custom Element
dcefor custom element ^^datadomfor easier inspectionxmlas a stringxsltas a string
template debugging
xml and xslt can be saved to file via for "copy string contents" into clipboard.
The XSLT debugger from your favorite IDE can set the breakpoints withing those files and run transformation under debugger.
{} does not give a value
- try to add as attribute you could observe and put the value of node name or text to identify the current location in data within template