What Does "Platform" Really Signify?
At its core, a platform is the foundation upon which software runs — it supplies both the physical hardware and the necessary software layer to execute applications.
Traditionally, the term "platform" denoted a computer's operating system. A program on a PC ran on the Windows platform, whereas something on an iMac operated on the Macintosh platform. Nowadays, numerous platforms sit atop operating systems, with the web and mobile being the most prominent. Web components belong to the web platform.
In front-end development, our code is executed by browsers. Therefore, the web platform manifests as the browser itself. It's the browser's responsibility to interface with the native OS and furnish the tools required for logic execution, I/O operations, and rendering. Browser vendors are tasked with implementing platform features so our applications can function.
However, who dictates which features get built? Who decides the APIs for network calls, file access, or navigation? And how do browsers determine what to implement? All web platform capabilities are outlined in specifications (standards), crafted by communities often referred to as committees. For web standards, two bodies stand out: the World Wide Web Consortium (W3C) and the Web Hypertext Application Technology Working Group (WHATWG). You can delve into their operations and the proposal lifecycle in this detailed article.
With a clear understanding of the web platform and its mechanics, let's explore the most thrilling web components proposals of 2019.
Microsoft Edge Switches to Chromium
One of the hottest topics recently has been Microsoft's decision to adopt the Chromium engine for Microsoft Edge. This move ensures Edge gets fully up-to-date implementations of all specifications enabling Web Components, notably the compelling capability of customizing built-in elements. Furthermore, the Chromium engine brings a suite of other impressive features including Template Instantiation, CSS Shadow Parts, Constructable Stylesheets, CSS Modules, and Scoped Custom Element Definitions. Let's preview each.
Template Instantiation with Substituted Values
This remarkable feature would let a component's view render using native browser mechanics. Currently, web frameworks enable defining templates with expressions tied to component state. Change detection in Angular or reconciliation in React evaluates these expressions and uses the results for screen rendering. Frameworks spare us the tedious manual DOM creation and updates caused by state changes. How can the native platform step in?
We've had the template element since HTML5, but it lacks a native mechanism to instantiate DOM with substituted values during creation. That's changing. The new proposal outlines a method to instantiate and update templates by supplying values for placeholders.
Once implemented, we could define a template with placeholders for name and email like this:
<template id="person">
<section>
<h1>{{name}}</h1>
Email: <a href="mailto:{{email}}">{{email}}</a>
</section>
</template>
Then supply the placeholder values during instantiation:
let template = document.querySelector('#person');
let instance = template.createInstance({name: "Ryosuke Niwa", email: "rniwa@webkit.org"});
The createInstance method returns a TemplateInstance with a content property of DocumentFragment type. You'd use this property to grab the resulting DOM and append it to the Shadow DOM like this:
shadowRoot.appendChild(instance.content);
leaving the browser to render it.
The proposal also introduces an update method on the resulting content object, simplifying change detection. Just add a setter to intercept assignments and update the template as a side effect:
class Person extends HTMLElement {
constructor() {
super();
let template = document.querySelector('#person');
this.dom = template.createInstance({name: "Ryosuke Niwa", email: "rniwa@webkit.org"});
var shadow = this.attachShadow({mode: 'open'});
shadow.appendChild(this.dom);
}
set name(value) { this.dom.update({name: value}) }
set email(value) { this.dom.update({email: value}) }
}
This is merely one application of this mechanism. For deeper insights, check the proposal here and here.
CSS Shadow Parts
The web platform keeps evolving to grant user code powers once reserved for native elements. We gained the ability to define new element types via Custom Elements. Then Shadow DOM arrived, offering true encapsulation—meaning styles from outside can't touch elements inside the Shadow DOM. As this demo shows, styles on span elements couldn't breach the Shadow DOM boundary:
<html>
<head>
<style>
span { color: red; }
</style>
</head>
<body>
<span>color will be red</span>
<div class="shadow">
#shadow-root
<span>color will black</span>
</div>
</body>
</html>
Now, a new pseudo-element joins CSS selectors to simplify styling inside Shadow DOM—e.g., applying custom styles to a reusable component from a shared library.
This pseudo-element is ::part(). It targets elements exposed via a part attribute.
This lets shadow hosts selectively expose specific elements from their DOM for external styling. Unlike custom properties, it functions on HTML elements, and unlike piercing operators *\*/deep/ or :shadow**, it offers precise control over which elements can be styled. **For more on custom properties or piercing operators, read here.
To use this, mark HTML elements with the part attribute:
<x-foo>
#shadow-root
<div part="some-box"><span>...</span></div>
<input part="some-input">
<div>...</div> /_ not styleable _/
</x-foo>
Then target those elements from outside the Shadow DOM:
x-foo::part(some-box) { ... }
This lands in Chrome 74.
Constructable Stylesheet Objects
Due to encapsulation, top-level document styles can't reach elements inside Shadow DOM. To style Shadow DOM content, styles must currently live inside it via the style element:
<x-foo>
#shadow-root
<style>p { color: green; }</style>
<div part="some-box"><span>...</span></div>
<input part="some-input">
<div>...</div> /_ not styleable
<x-foo>
_#shadow-root_
<style>p { color: green; }</style>
<div part="some-box"><span>...</span></div>
<input part="some-input">
<div>...</div> /_ not styleable
</x-foo>
</x-foo>
The downside: browsers parse and store style rules for each style element. With Shadow DOM integral to web components, every component carries a style element. Reusing a component on a page means duplicating styles, so parsing and storing stylesheets per instance is wasteful. With pages containing tens of thousands of components, this incurs significant time and memory costs. Browsers try to optimize by deduplicating based on string content, skipping re-parsing for identical styles. Yet the issue vanishes entirely with imperative stylesheet creation and reuse.
The current proposal offers an API to create stylesheet objects from script, eliminating the need for declarative style elements. It also specifies how to reuse stylesheets across multiple spots. Here's the approach: first, create a stylesheet:
const myElementSheet = new CSSStyleSheet();
then attach it to the Shadow DOM:
shadowRoot.adoptedStyleSheets = [myElementSheet];
Each stylesheet object can be added to any number of shadow roots (or the top-level document).
The proposal also defines APIs to add, remove, or replace rules within a stylesheet. The simplest way to initialize one is via the replaceSync method, passing a string with stylesheet rules:
const styles = 'p { color: green; }';
myElementSheet.replaceSync(styleText)
This example shows the functionality inside a web component:
class MyElement extends HTMLElement {
constructor() {
super();
const shadowRoot = this.attachShadow({ mode: "open" });
shadowRoot.adoptedStyleSheets = [myElementSheet];
}
connectedCallback() {
if (myElementSheet.cssRules.length == 0) {
myElementSheet.replaceSync(styleText);
}
}
}
Notice the stylesheet parsing starts in the connectedCallback when the first component instance connects to the DOM. Read more about the spec here.
HTML Modules & CSS Modules
With ECMAScript 6, native modules became part of the JavaScript ecosystem, eliminating the need for third-party loaders. In a script loaded via the type="module" attribute, you can use:
import toUpperCase from './uppercase.js'
and let the browser handle loading, parsing, and instantiating the module code automatically.
Yet, bringing HTML templates or stylesheets onto a page still requires separate HTML and CSS loaders. What if you could use syntax like this in an ES module script, with the browser taking care of loading and instantiating the HTML template and CSS stylesheet?
import styles from './mycomponent.css';
import template from './mycomponent.html";
That's precisely what the HTML Modules (initial) and CSS Modules proposals describe.
HTML Modules, originally called HTML imports, were first proposed and implemented in Chromium, but they were built separately from ES6, which brought several drawbacks. The newly suggested implementation is outlined in the explainer doc from the Edge team. It demonstrates how HTML Modules can fit into the current ES6 module system rather than standing alone.
The import statements already used for Script Modules will also work for HTML Modules:
<script type="module">
import {content} from "import.html"
document.body.appendChild(content);
</script>
To define what an HTML Module exports, you rely on its inline script elements:
<div id="blogPost">
<p>Content...</p>
</div>
<script type="module">
let blogPost = import.meta.document.querySelector("#blogPost");
export {blogPost}
</script>
CSS Modules introduces something entirely new.
Importing a stylesheet follows the same import pattern as HTML Modules:
import styles from './styles.css';
// push() doesn't actually exist yet
document.styleSheets.push(styles);
The single export from a CSS module is a default export of the StyleSheet object. The rules for CSS Modules are straightforward; when paired with Constructable Stylesheets mentioned earlier, the importer controls how the CSS affects the document.
Here's an illustration of both in action inside a Web Component:
import {content} from "import.html"
import styles from './styles.css';
class MyElement extends HTMLElement {
constructor() {
this.attachShadow({mode: open});
// push() doesn't actually exist yet this.shadowRoot.moreStyleSheets.push(styles);
this.shadowRoot.appendChild(content);
}
}
CSS Modules ships in Chrome 73.
Scoped Custom Element Definitions
To register a custom element today, you use the define method on the CustomElementRegistry, accessible via window.customElement:
class WordCount extends HTMLParagraphElement { ... };
customElements.define('word-count', WordCount, ...);
The registry exposed on window is global, which creates potential name conflicts. These could arise by accident, from an app defining multiple versions of the same element, or in more complex cases like testing with mocks or a component deliberately overriding an element definition within its own context.
The Scoped Custom Element Registries proposal offers a fix for this issue. It enables creating CustomElementRegistry instances imperatively and chaining them to inherit element definitions. ShadowRoot acts as the scope for these definitions. A ShadowRoot can be tied to a CustomElementRegistry at creation time and gains element creation functions like createElement. When elements are created inside a ShadowRoot, that root's registry governs Custom Element upgrades.
This example shows how to build a custom element registry and link it to the Shadow DOM:
// Create a new registry that inherits from the global registry
const myRegistry = new CustomElementRegistry(window.customElements);
// Use the local registry when creating the ShadowRoot
element.attachShadow({mode: 'open', customElements: myRegistry});
After that, you can register a custom element on this registry and use the scoped creation APIs to produce elements:
// Define a trivial subclass of XFoo so that we can register it ourselves
class MyFoo extends XFoo {}
// Register it as `my-foo` locally.
myRegistry.define('my-foo', MyFoo);
// the definiton of my-foo will be resolved from the custom registry
const myFoo = this.shadowRoot.createElement('my-foo');
element.shadowRoot.appendChild(myFoo);
