- Custom Elements
- Custom built-in elements (currently unsupported)
- Shadow DOM
- HTML Import
- HTML Template
- Polyfills
- Browser support
- Resources
- A custom element, a.k.a an autonomous custom element, should be defined with an ES6 class extending
HTMLElement
. - The name of a custom element must contain a dash (-) character, this is to prevent clashes with future built-in elements.
- A custom element must be registered with the
CustomElementRegistry
.
class MyCustomElement extends HTMLElement {
static get observedAttributes() { /* use only when necessary */ }
constructor() {
super();
/* use only when necessary */
}
connectedCallback() { /* use only when necessary */ }
disconnectedCallback() { /* use only when necessary */ }
adoptedCallback() { /* use only when necessary */ }
attributeChangedCallback() { /* use only when necessary */ }
}
// an autonomous custom element
window.customElements.define("my-custom-element", MyCustomElement);
- An autonomous custom element applies the defined behaviour to a custom tag.
<my-custom-element></my-custom-element>
- The constructor is run when the element is 'upgraded', i.e. it is both registered with the
CustomElementRegistry
and added to the DOM. - A no-args call to
super()
must be the first statement. - The constructor should be used only for setting up initial state and default values, event listeners and possibly a shadow root.
- Work should be deferred to the
connectedCallback
as much as possible.
Called when the element is registered and inserted in the DOM. This is the appropriate place to do actual (time-consuming) work like fetching resources or rendering.
Note that this callback can be called more than once, namely, every time the element is (re)inserted into the DOM.
Called every time the element is detached from the DOM. Intended for cleanup purposes.
Called when the element is adopted into a new document through the document.adoptNode
method.
Called whenever any of the observedAttributes
(see below) is changed.
class FlagIcon extends HTMLElement {
// ...
attributeChangedCallback(name, oldValue, newValue) {
if ("country".equals(name)) {
this._countryCode = newValue;
}
}
// ...
}
Exposes an array of attribute names for which the attributeChangedCallback
should be called whenever their values are changed.
class FlagIcon extends HTMLElement {
// ...
static get observedAttributes() { return ["country"]; }
// ...
}
The :defined
pseudo-class applies to all elements that are known to the user agent, i.e. it applies to all elements that are either built-in or successfully upgraded to custom elements.
This allows, for example, for hiding any non-functional custom elements, while the element definitions for the non-essential elements are being downloaded asynchronously to reduce the initial load time.
my-custom-element:not(:defined) {
display: block;
height: 100vh;
opacity: 0;
}
Note that the :defined pseudo class cannot be polyfilled, so it will only work for user agents that implement the custom element v1 specifications natively. However, you can mimic this behaviour with, for example, an attribute that is removed once the element is connected.
my-custom-element[unresolved] {
display: block;
height: 100vh;
opacity: 0;
}
class MyCustomElement extends HTMLElement {
// ...
connectedCallback() {
this.removeAttribute('unresolved');
}
// ...
}
<my-custom-element unresolved></my-custom-element>
- An element extending a built-in element is known as a customized built-in element.
- It is intended to work exactly like an autonomous custom element, but inheriting all behavior from its parent built-in element.
- Rather than extending
HTMLElement
itself, a customized built-in element extends one of the corresponding subclasses (likeHTMLButtonElement
), and it supplies a configuration object to specify what element is extended (since multiple elements can use the same interface class).
class MyCustomButton extends HTMLButtonElement {
// ...
}
window.customElements.define("my-custom-button", MyCustomButton, { extends: "button" });
- Instead of a custom tag name, the tag name that corresponds to the parent class is used.
It is then enhanced with the
is
property, set to the name of the customized built-in element.
<button is="my-custom-button">Click me</x-button>
- The Shadow DOM defines a scope within which styles are applicable. Together with the custom element spec, this allows for webcomponents with self contained HTML, CSS, and JS.
- A component's DOM is self-contained (e.g.
document.querySelector()
will not return nodes in the component's Shadow DOM). Thus, simpleid
s and class names are enough for identification, because they will not clash with the containing document. - Shadow DOM can be attached to all elements, except to those that already host their own Shadow DOM (
<texarea>
,input
), or to those for which it does not make sense (<img>
).
const header = document.createElement('header');
const shadowRoot = header.attachShadow({ mode: 'open' });
const caption = document.createElement('h1');
caption.appendChild(document.createTextNode('Hello Shadow DOM'));
shadowRoot.appendChild(caption);
- A Shadow DOM tree can contain
<style>
and even<link rel="stylesheet">
. These styles are local to the Shadow DOM and will not bleed into the document.
- The markup within a custom element is called 'Light DOM'.
- By default, when a Shadow DOM tree is attached to an element, its Light DOM is ignored.
- If the Shadow DOM has slots, all elements in the Light DOM are distributed over those slots, to create a 'Flattened DOM tree'.
- Slots can have a name and default content.
The name is used to identify a specific slot for Light DOM content.
If there is no Light DOM to fit in a slot, its default content (any markup inside
<slot>
) is used. - The slot with no name is the default slot, and all elements in the Light DOM that do not specify a slot are moved there. Note: Technically you can have multiple unnamed slots, but only the first one will ever be populated.
<div>
<!-- this is the Light DOM -->
<span>foo</span>
<span slot="bar">bar</span>
<span slot="nonexistent">qux</span>
<span>quux</span>
</div>
<!-- Visual representation of the Shadow DOM -->
<h1>
<slot>Header here</slot>
</h1>
<slot name="bar"></slot>
<p>
<slot name="unused">No Light DOM for me!</slot>
</p>
<!-- Result, a.k.a. the flattened DOM tree -->
<!--
* The 'foo' and 'quux' span are moved to the default slot, replacing the default content.
* The 'bar' span is moved to the 'bar' slot.
* The 'qux' span is ignored as its specified slot does not exist.
* The 'unused' slot shows the default content as no Light DOM was moved there
-->
<div>
<h1>
<slot>
<span>foo</span>
<span>quux</span>
</slot>
</h1>
<slot name="bar">
<span slot="bar">bar</span>
</slot>
<p>
<slot name="unused">No Light DOM for me!</slot>
</p>
</div>
- CSS selectors from the outer page do not apply inside the component, and vice versa.
This allows for the use of simpler class names and
id
s as they will not conflict, which also has a positive effect on performance. - Styles can be defined within the Shadow DOM for the elements from the Light DOM, however the styles from the surrounding document take precedence.
- The Shadow DOM can exert limited influence on the host element (the element to which the Shadow tree is attached) and the Light DOM that is distributed into its slots. However, in all cases, styling applied in the surrounding document will take precedence.
:host
selects the host element.:host(<selector>)
selects the host element only if the given<selector>
matches the host.:host-context(<compound-selector>)
selects the host element only if the given<compound-selector>
matches the host or any of its ancestors. This allows, for example, for having themes across the page by toggling a class on the body element (although CSS custom properties may be preferable here).::slotted(<compound-selector>)
selects any top level element from the Light DOM that matches the<compound-selector>
and is distributed into any of the Shadow DOM's slots.
- The surrounding document can not influence the styling of the Shadow DOM, unless the Shadow DOM supplies styling hooks through CSS custom properties.
- CSS custom properties are required to have two dashes before the name (
--*
), and can have any value. - They can be picked up using
var(--foo)
, orvar(--foo, <fallback-value>)
.
<style>
div {
/* fallback if custom properties are not supported (IE11) */
color: green
--header-color: green;
}
</style>
<div id="div-host"></div> <!-- green -->
<span id="span-host"></span> <!-- red -->
<!-- Visual representation of the Shadow DOM -->
<style>
h1 {
color: var(--header-color, red);
}
</style>
<h1>Hello World</h1>
- HTML imports allow fragments of HTML to be inserted into another document.
It does not need to be a full HTML page (with a
<head>
,<body>
, etc) but may contain anything that is allowed within HTML, like markup content, script, styling, and HTML imports of their own, of course. - The MIME type of the import is
text/html
. - You can define an HTML import by declaring a
<link rel="import">
<head>
<!-- external resources need to be CORS enabled -->
<link rel="import" href="/path/to/imports/stuff.html">
</head>
- Or you can create one with JavaScript.
const link = document.createElement('link');
link.rel = 'import';
link.href = 'file.html';
document.head.appendChild(link);
- An HTML import will not add anything to the document, but it will make its contents available for later use, instead.
const content = document.querySelector('link[rel="import"]').import;
- Styling will be applied, and script will be executed when it is encountered.
- Imports are de-duplicated automatically, meaning that multiple declarations of an import from the same URL will only trigger retrieval and parsing once, thus the scripts will only execute the first time.
- By default, downloading of imports is blocking, like downloading stylesheets, although parsing of the document may continue. This preserves execution order of scripts and helps to prevent a FOUC (Flash Of Unstyled Content), because imports may contain styling.
- Retrieval can be done asynchronously by setting the
async
property. - It may be useful to also provide an
onload
, and possibly anonerror
callback.
<script>
/* the callbacks need to be known before they are used */
function handleLoad(e) {
console.log('Loaded import: ' + e.target.href);
}
function handleError(e) {
console.log('Error loading import: ' + e.target.href);
}
</script>
<link rel="import" href="file.html" async
onload="handleLoad(event)" onerror="handleError(event)">
- Scripts in the import are executed from the context of the window that contains the imported document.
This means that top-level functions defined in the import are added to
window
for everyone to use, anddocument
refers to the main document. - The
document
of the import itself can be retrieved throughdocument.currentScript.ownerDocument
. However,currentScript
is only available when the script is initially being processed. So if you need theownerDocument
for code executing within a callback or event handler you'll have to store it for later use. - The HTML import polyfill exposes the
document
throughdocument._currentScript.ownerDocument
(note the underscore). It will add anHTMLImports.useNative
property to be able to detect whichownerDocument
should be used.
main.html
<!DOCTYPE html>
<html>
<head>
<title>Import test</title>
<link rel="import" href="/theimport.html">
</head>
<body>
<h1>Hello world</h1>
<script>
const content = document.querySelector('link[rel="import"]').import;
const imported = content.querySelector('h1');
/* this will copy the header from the import */
document.body.appendChild(imported.cloneNode(true));
console.log(!!content.querySelector('h1')); /* true */
/* this will yank the header out of the import */
document.body.appendChild(imported);
console.log(!!content.querySelector('h1')); /* false */
</script>
</body>
</html>
theimport.html
<link rel="stylesheet" href="importsheet.css" type="text/css">
<style>
h1 {
/* this is applied to both headers */
color: blue;
}
</style>
<h1>Hello imported world</h1>
<script>
/* document refers to the main document */
const headers = document.querySelectorAll('h1');
/*
* This will only change the header in the main document,
* as the header defined here will not be imported yet
* due to the execution order.
*/
headers.forEach((h) => {
h.style.fontFamily = "fantasy";
});
/*
* This will specifically select the header in this document
* and apply the styling before it is imported.
*/
const thisHeader = document.currentScript.ownerDocument.querySelector('h1');
thisHeader.style.border = "1px dashed";
thisHeader.style.display = "inline";
</script>
importsheet.css
h1 {
/* this is applied to both headers */
font-style: italic;
}
-
De-duplication allows for a form of dependency management, i.e. including jQuery multiple times will only result in jQuery being downloaded once. However, this only works when the URL is the exact same.
-
Due to the upcoming ES6 modules implementation across browsers, which is expected to influence the direction the HTML import specification will take to adress the concerns with dependency management, Mozilla has decided not to implement the current draft specification. Their reasons are explained in an email discussion by Anne van Kesteren and Boris Zbarsky. However, due to the existence of a polyfill, this is not seen as an impediment for using webcomponents.
-
When writing your own component, you may want to include resources that are served from your component's location. However, in the current specification, references (such as
src
,href
, and, in CSS,url
) are resolved relative to the main document (where your component is imported), and not to your component's location.It is possible to work around this without having to resort to absolute paths, but this requires rewriting all references with bindings or custom logic. This works by using
document.currentScript.ownerDocument.baseUri
to find the location of the current component, as opposed to the location of the main document where the component is imported.
- HTML template is the first of the WebComponent specs to be implemented by all browser vendors, only IE11 requires a polyfill.
- An HTML template is indicated with the
<template>
tag, and can be placed virtually anywhere in the DOM. - The content of the
<template>
tag is put into a different HTML document fragment by the parser and as such is completely inert. This means script won't run, markup won't render, and styles won't apply. It does not even have to be valid HTML. - The
content
property of a template contains the template document fragment and can be imported or cloned. Adding the clone to the DOM will activate the contents of that template, and at that time it has to be valid HTML.
const clone = template.content.cloneNode(true);
const clone2 = document.importNode(template.content, true);
- As the content of the template lives in a different document fragment, it will not show up when selecting nodes. You need to specifically go through the template's content property to select it.
<h1>I'm in the main document</h1>
<template>
<h1>I'm in a template</h1>
</template>
<script>
const template = document.querySelector('template');
console.log(document.querySelectorAll('h1').length); // 1
console.log(template.content.querySelectorAll('h1').length); // 1
document.body.appendChild(template.content.cloneNode(true));
console.log(document.querySelectorAll('h1').length); // 2
</script>
- All modern browsers have at least indicated to be working towards implementing the four specifications that webcomponents consist of. In the mean time there are polyfills available to patch the gaps at https://github.com/WebComponents/webcomponentsjs.
- The polyfills are applied asynchronously, so to be sure everything is ready you should move any logic that requires the polyfills to be in place into the custom
WebComponentsReady
event. If you are working with lower level polyfills you may have to wait for theDOMContentLoaded
event.
window.addEventListener('WebComponentsReady', function(e) {
// imports are loaded and elements have been registered
console.log('Components are ready');
});
- There are several methods available to make sure (only) the necessary polyfills are included.
webcomponentsjs/webcomponents-loader.js
does feature detection to see which polyfills are necessary, and downloads only those for the cost of one extra request.webcomponentsjs/webcomponents-lite.js
contains all the polyfills, using only those that are actually necessary, and as such will work in all cases but may be a heavier download than necessary.- Have the server serve the exact set necessary for the browser making the request. This is the option with the minimal download and requests but will require the server to be capable of serving different assets based on user agent, which may be more hassle than it is worth.
- The specification of Custom Elements requires ES6 support, but not all browsers support this (IE11).
WebComponents can be transpiled to ES5 and provide the same functionality to those browsers, however if, for simplicity, the transpiled sources are used for all browsers it is necessary to include
/webcomponentsjs/custom-elements-es5-adapter.js
. This is a shim for browsers that support Custom Elements natively to make sure things won't break for them. - Note that the polyfills and the shim don't need to be, and should not be, transpiled.
- β : natively supported
- πΆ: supported with polyfill
- β: not supported
Desktop | Mobile | ||||||||
Chrome | FireFox | Edge | IE11 | Safari | Opera | IOS Safari | Android Browser | Chrome for Android | |
Custom Elements (v1) | β | πΆ | πΆ | πΆ | β | β | β | β | β |
Shadow DOM | β | πΆ | πΆ | πΆ | β | β | β | β | β |
HTML Import | β | πΆ | πΆ | πΆ | πΆ | β | πΆ | β | β |
HTML Template | β | β | β | πΆ | β | β | β | β | β |
CSS custom properties | β | β | β | β | β | β | β | β | β |
- Custom Element Specification
- Shadow DOM v1: Self-Contained Web Components
- Open vs Closed Shadow DOM
- WebComponents polyfill repository
- HTML Imports #include for the web
- The Problem With Using HTML Imports For Dependency Management
- Mozilla and Web Components: Update
- Re: HTML imports in Firefox (Anne van Kesteren)
- Re: HTML imports in Firefox (Boris Zbarsky)
- Polymer: Billions Served; Lessons Learned (Google I/O '17)
- Browser support overview
- URLs in templates