title: The state of Web Components
url: https://hacks.mozilla.org/2015/06/the-state-of-web-components/
hash_url: d3c5f2dc3f30a43b13c8662ac468e889

<p>Web Components have been on developers&#8217; radars for quite some time now. They were first introduced by <a href="http://twitter.com/slightlylate">Alex Russell</a> at<a href="https://fronteers.nl/congres/2011/sessions/web-components-and-model-driven-views-alex-russell"> Fronteers Conference 2011</a>. The concept shook the community up and became the topic of many future talks and discussions.</p>
<p>In 2013 a Web Components-based framework called <em>Polymer</em> was released by <em>Google</em> to kick the tires of these new APIs, get community feedback and add some sugar and opinion.</p>
<p>By now, 4 years on, Web Components <em>should</em> be everywhere, but in reality <em>Chrome</em> is the only browser with &#8216;some version&#8217; of Web Components. Even with polyfills it&#8217;s clear Web Components won&#8217;t be fully embraced by the community until the majority of browsers are on-board.</p>
<h2>Why has this taken so long?</h2>
<p>To cut a long story short, vendors couldn&#8217;t agree.</p>
<p>Web Components were a <em>Google</em> effort and little negotiation was made with other browsers before shipping. Like most negotiations in life, parties that don&#8217;t feel involved lack enthusiasm and tend not to agree.</p>
<p>Web Components were an ambitious proposal. Initial APIs were high-level and complex to implement (albeit for good reasons), which only added to contention and disagreement between vendors.</p>
<p><em>Google</em> pushed forward, they sought feedback, gained community buy-in; but in hindsight, before other vendors shipped, usability was blocked.</p>
<p>Polyfills meant theoretically Web Components could work on browsers that hadn&#8217;t yet implemented, but these have never been accepted as &#8216;suitable for production&#8217;.</p>
<p>Aside from all this, <em>Microsoft</em> haven&#8217;t been in a position to add many new DOM APIs due to the <a href="http://www.microsoft.com/en-us/windows/browser-for-doing"><em>Edge</em></a> work (nearing completion). And <em>Apple</em>, have been focusing on alternative features for <em>Safari</em>.</p>
<h2>Custom Elements</h2>
<p>Of all the Web Components technologies, Custom Elements have been the least contentious. There is general agreement on the value of being able to define how a piece of UI looks and behaves and being able to distribute that piece cross-browser and cross-framework.</p>
<h3>&#8216;Upgrade&#8217;</h3>
<p>The term &#8216;upgrade&#8217; refers to when an element transforms from a plain old <code>HTMLElement</code> into a shiny custom element with its defined life-cycle and <code>prototype</code>. Today, when elements are upgraded, their <code>createdCallback</code> is called.</p>
<pre><code class="js">var proto = Object.create(HTMLElement.prototype);
proto.createdCallback = function() { ... };
document.registerElement('x-foo', { prototype: proto });</code></pre>
<p>There are <a href="https://wiki.whatwg.org/wiki/Custom_Elements#Upgrading">five proposals</a> so far from multiple vendors; two stand out as holding the most promise.</p>
<h4>&#8216;Dmitry&#8217;</h4>
<p>An evolved version of the <code>createdCallback</code> pattern that works well with ES6 classes. The <code>createdCallback</code> concept lives on, but sub-classing is more conventional.</p>
<pre><code class="js">class MyEl extends HTMLElement {
  createdCallback() { ... }
}

document.registerElement("my-el", MyEl);</code></pre>
<p>Like in today&#8217;s implementation, the custom element begins life as <code>HTMLUnknownElement</code> then some time later the prototype is swapped (or &#8216;swizzled&#8217;) with the registered prototype and the <code>createdCallback</code> is called.</p>
<p>The downside of this approach is that it&#8217;s different from how the platform itself behaves. Elements are &#8216;unknown&#8217; at first, then transform into their final form at some point in the future, which can lead to developer confusion.</p>
<h4>Synchronous constructor</h4>
<p>The constructor registered by the developer is invoked by the parser at the point the custom element is created and inserted into the tree.</p>
<pre><code class="js">class MyEl extends HTMLElement {
  constructor() { ... }
}

document.registerElement("my-el", MyEl);</code></pre>
<p>Although this seems sensible, it means that any custom elements in the initial downloaded document will fail to upgrade if the scripts that contain their <code>registerElement</code> definition are loaded asynchronously. This is not helpful heading into a world of asynchronous ES6 modules.</p>
<p>Additionally synchronous constructors come with <a href="https://lists.w3.org/Archives/Public/public-webapps/2015JanMar/0731.html">platform issues</a> related to <code>.cloneNode()</code>.</p>
<p>A direction is expected to be decided by vendors at a face-to-face meeting in July 2015.</p>
<h3>is=&#8221;&#8221;</h3>
<p>The <code>is</code> attribute gives developers the ability to layer the behaviour of a custom element on top of a standard built-in element.</p>
<pre><code class="html">&lt;input type="text" is="my-text-input"&gt;</code></pre>
<h4>Arguments for</h4>
<ol>
<li>Allows extending the built-in features of a element that aren&#8217;t exposed as primitives (eg. accessibility characteristics, <code>&lt;form&gt;</code> controls, <code>&lt;template&gt;</code>).</li>
<li>They give means to &#8216;progressively enhance&#8217; an element, so that it remains functional without JavaScript.</li>
</ol>
<h4>Arguments against</h4>
<ol>
<li>Syntax is confusing.</li>
<li>It side-steps the underlying problem that we&#8217;re <a href="https://github.com/domenic/html-as-custom-elements/blob/master/docs/accessibility.md">missing many key accessibility primitives</a> in the platform.</li>
<li>It side-steps the underlying problem that we don&#8217;t have a way to properly extend built-in elements.</li>
<li>Use-cases are limited; as soon as developers introduce Shadow DOM, they lose all built-in accessibility features.</li>
</ol>
<h4>Consensus</h4>
<p>It is generally agreed that <code>is</code> is a &#8216;wart&#8217; on the Custom Elements spec. <em>Google has</em> already implemented <code>is</code> and sees it as a stop-gap until lower-level primitives are exposed. Right now <em>Mozilla</em> and <em>Apple</em> would rather ship a Custom Elements V1 sooner and address this problem properly in a V2 without polluting the platform with &#8216;warts&#8217;.</p>
<p><a href="https://github.com/domenic/html-as-custom-elements"><em>HTML as Custom Elements</em></a> is a project by Domenic Denicola that attempts to rebuild built-in HTML elements with custom elements in an attempt to uncover DOM primitives the platform is missing.</p>
<h2>Shadow DOM</h2>
<p>Shadow DOM yielded the most contention by far between vendors. So much so that features had to be split into a &#8216;V1&#8242; and &#8216;V2&#8242; agenda to help reach agreement quicker.</p>
<h3>Distribution</h3>
<p>Distribution is the phase whereby children of a shadow host get visually &#8216;projected&#8217; into slots inside the host&#8217;s Shadow DOM. This is the feature that enables your component to make use of content the user nests inside it.</p>
<h4>Current API</h4>
<p>The current API is fully declarative. Within the Shadow DOM you can use special <code>&lt;content&gt;</code> elements to define where you want the host&#8217;s children to be visually inserted.</p>
<pre><code class="html">&lt;content select="header"&gt;&lt;/content&gt;</code></pre>
<p>Both <em>Apple</em> and <em>Microsoft</em> pushed back on this approach due to concerns around complexity and performance.</p>
<h4>A new Imperative API</h4>
<p>Even at the <a href="https://www.w3.org/wiki/Webapps/WebComponentsApril2015Meeting">face-to-face meeting</a>, agreement couldn&#8217;t be made on a declarative API, so all vendors agreed to pursue an imperative solution.</p>
<p>All four vendors (<em>Microsoft</em>, <em>Google</em>, <em>Apple</em> and <em>Mozilla</em>) were tasked with specifying this new API before a July 2015 deadline. So far there have been <a href="https://github.com/w3c/webcomponents/blob/gh-pages/proposals/Imperative-API-for-Node-Distribution-in-Shadow-DOM.md">three suggestions</a>. The simplest of the three looks something like:</p>
<pre><code class="js">var shadow = host.createShadowRoot({
  distribute: function(nodes) {
    var slot = shadow.querySelector('content');
    for (var i = 0; i &lt; nodes.length; i++) {
      slot.add(nodes[i]);
    }
  }
});

shadow.innerHTML = '&lt;content&gt;&lt;/content&gt;';

// Call initially ...
shadow.distribute();

// then hook up to MutationObserver</code></pre>
<p>The main obstacle is:<strong> timing</strong>. If the children of the host node change and we redistribute when the <code>MutationObserver</code> callback fires, asking for a layout property will return an incorrect result.</p>
<pre><code class="js">myHost.appendChild(someElement);
someElement.offsetTop; //=&gt; old value

// distribute on mutation observer callback (async)

someElement.offsetTop; //=&gt; new value</code></pre>
<p>Calling offsetTop will perform a synchronous layout <em>before</em> distribution!</p>
<p>This might not seems like the end of the world, but scripts and browser internals often depend on the value of <code>offsetTop</code> being correct to perform many different operations, such as: scrolling elements into view.</p>
<p>If these problems can&#8217;t be solved we may see a retreat back to discussions over a declarative API. This will either be in the form of the current <code>&lt;content select&gt;</code> style, or the newly proposed <a href="https://github.com/w3c/webcomponents/blob/gh-pages/proposals/Slots-Proposal.md">&#8216;named slots&#8217;</a> API (from <em>Apple</em>).</p>
<h4>A new Declarative API &#8211; &#8216;Named Slots&#8217;</h4>
<p>The &#8216;named slots&#8217; proposal is a simpler variation of the current &#8216;content select&#8217; API, whereby the component user must explicitly label their content with the slot they wish it to be distributed to.</p>
<p>Shadow Root of &lt;x-page&gt;:</p>
<pre><code class="html">&lt;slot name="header"&gt;&lt;/slot&gt;
&lt;slot&gt;&lt;/slot&gt;
&lt;slot name="footer"&gt;&lt;/slot&gt;
&lt;div&gt;some shadow content&lt;/div&gt;
</code></pre>
<p>Usage of &lt;x-page&gt;:</p>
<pre><code class="html">&lt;x-page&gt;
  &lt;header slot="header"&gt;header&lt;/header&gt;
  &lt;footer slot="footer"&gt;footer&lt;/footer&gt;
  &lt;h1&gt;my page title&lt;/h1&gt;
  &lt;p&gt;my page content&lt;p&gt;
&lt;/x-page&gt;</code></pre>
<p>Composed/rendered tree (what the user sees):</p>
<pre><code class="html">&lt;x-page&gt;
  &lt;header slot="header"&gt;header&lt;/header&gt;
  &lt;h1&gt;my page title&lt;/h1&gt;
  &lt;p&gt;my page content&lt;p&gt;
  &lt;footer slot="footer"&gt;footer&lt;/footer&gt;
  &lt;div&gt;some shadow content&lt;/div&gt;
&lt;/x-page&gt;</code></pre>
<p>The browser has looked at the direct children of the shadow host (<code>myXPage.children</code>) and seen if any of them have a slot attribute that matches the name of a &lt;slot&gt; element in the host&#8217;s <code>shadowRoot</code>.</p>
<p>When a match is found, the node is <em>visually</em> &#8216;distributed&#8217; in place of the corresponding &lt;slot&gt; element. Any children left undistributed at the end of this matching process are distributed to a default (unamed) &lt;slot&gt; element (if one exists).</p>
<h5>For:</h5>
<ol>
<li>Distribution is more explicit, easier to understand, less &#8216;magic&#8217;.</li>
<li>Distribution is simpler for the engine to compute.</li>
</ol>
<h5>Against:</h5>
<ol>
<li>Doesn&#8217;t explain how built-in elements, like &lt;select&gt;, work.</li>
<li>Decorating content with slot attributes is more work for the user.</li>
<li>Less expressive.</li>
</ol>
<h3>&#8216;closed&#8217; vs. &#8216;open&#8217;</h3>
<p>When a <code>shadowRoot</code> is &#8216;closed&#8217; the it cannot be accessed via <code>myHost.shadowRoot</code>. This gives a component author <em>some</em> assurance that users won&#8217;t poke into implementation details, similar to how you can use closures to keep things private.</p>
<p><em>Apple</em> felt strongly that this was an important feature that they would block on. They argued that implementation details should never be exposed to the outside world and that &#8216;closed&#8217; mode would be a required feature when <a href="https://github.com/w3c/webcomponents/wiki/Isolated-Imports-Proposal">&#8216;isolated&#8217; custom elements</a> became a thing.</p>
<p><em>Google</em> on the other hand felt that &#8216;closed&#8217; shadow roots would prevent some accessibility and component tooling use-cases. They argued that it&#8217;s impossible to accidentally stumble into a <code>shadowRoot</code> and that if people want to they likely have a good reason. JS/DOM is open, let&#8217;s keep it that way.</p>
<p>At the <a href="https://www.w3.org/wiki/Webapps/WebComponentsApril2015Meeting">April meeting</a> it became clear that to move forward, &#8216;mode&#8217; needed to be a feature, but vendors were struggling to reach agreement on whether this should default to &#8216;open&#8217; or &#8216;closed&#8217;. As a result, all agreed that for V1 &#8216;mode&#8217; would be a required parameter, and thus wouldn&#8217;t need a specified default.</p>
<pre><code class="js">element.createShadowRoot({ mode: 'open' });
element.createShadowRoot({ mode: 'closed' });</code></pre>
<h3>Shadow piercing combinators</h3>
<p>A &#8216;piercing combinator&#8217; is a special CSS &#8216;combinator&#8217; that can target elements inside a shadow root from the outside world. An example is /deep/ later renamed to <code>&gt;&gt;&gt;</code>:</p>
<pre><code class="css">.foo &gt;&gt;&gt; div { color: red }</code></pre>
<p>When Web Components were first specified it was thought that these were required, but after looking at <a href="https://github.com/KarstenB/csstransform/blob/master/bootstrap_deep.css">how they were being used</a> it seemed to only bring problems, making it too easy to break the style boundaries that make Web Components so appealing.</p>
<h4>Performance</h4>
<p>Style calculation can be incredibly fast inside a tightly scoped Shadow DOM if the engine doesn&#8217;t have to take into consideration any outside selectors or state. The very presence of piercing combinators forbids these kind of optimisations.</p>
<h4>Alternatives</h4>
<p>Dropping shadow piercing combinators doesn&#8217;t mean that users will never be able to customize the appearance of a component from the outside.</p>
<h5>CSS custom-properties (variables)</h5>
<p>In <em>Firefox OS</em> we&#8217;re using <a href="http://dev.w3.org/csswg/css-variables/">CSS Custom Properties</a> to expose specific style properties that can be defined (or overridden) from the outside.</p>
<p>External (user):</p>
<pre><code class="css">x-foo { --x-foo-border-radius: 10px; }
</code></pre>
<p>Internal (author):</p>
<pre><code class="css">.internal-part { border-radius: var(--x-foo-border-radius, 0); }</code></pre>
<h5>Custom pseudo-elements</h5>
<p>We have also seen interest expressed from several vendors in reintroducing the ability to define custom pseudo selectors that would expose given internal parts to be styled (similar to how we style parts of &lt;input type=&#8221;range&#8221;&gt; today).</p>
<pre class="fragment css visible current-fragment" data-fragment-index="0"><code class=" hljs " contenteditable=""><span class="hljs-tag">x-foo</span><span class="hljs-pseudo">::my-internal-part</span> <span class="hljs-rules">{ <span class="hljs-rule"><span class="hljs-attribute">... }</span></span></span></code></pre>
<p>This will likely be considered for a Shadow DOM V2 specification.</p>
<h5>Mixins &#8211; @extend</h5>
<p>There is <a href="https://tabatkins.github.io/specs/css-extend-rule/">proposed specification</a> to bring <a href="http://sass-lang.com/documentation/file.SASS_REFERENCE.html#extend">SASS&#8217;s @extend</a> behaviour to CSS. This would be a useful tool for component authors to allow users to provide a &#8216;bag&#8217; of properties to apply to a specific internal part.</p>
<p>External (user):</p>
<pre class="css"><code class=" hljs " contenteditable="" data-trim=""><span class="hljs-class">.x-foo-part</span> <span class="hljs-rules">{
  <span class="hljs-rule"><span class="hljs-attribute">background-color</span>:<span class="hljs-value"> red</span></span>;
  <span class="hljs-rule"><span class="hljs-attribute">border-radius</span>:<span class="hljs-value"> <span class="hljs-number">4px</span></span></span>;
<span class="hljs-rule">}</span></span></code></pre>
<p>Internal (author):</p>
<pre class="css"><code class=" hljs " contenteditable="" data-trim=""><span class="hljs-class">.internal-part</span> <span class="hljs-rules">{
  <span class="hljs-rule">@<span class="hljs-attribute">extend .x-foo-part;
}</span></span></span></code></pre>
<h3>Multiple shadow roots</h3>
<p><em>Why would I want more than one shadow root on the same element?</em>, I hear you ask. The answer is: <strong>inheritance</strong>.</p>
<p>Let&#8217;s imagine I&#8217;m writing an <code>&lt;x-dialog&gt;</code> component. Within this component I write all the markup, styling, and interactions to give me an opening and closing dialog window.</p>
<pre><code class="html">&lt;x-dialog&gt;
  &lt;h1&gt;My title&lt;/h1&gt;
  &lt;p&gt;Some details&lt;/p&gt;
  &lt;button&gt;Cancel&lt;/button&gt;
  &lt;button&gt;OK&lt;/button&gt;
&lt;/x-dialog&gt;</code></pre>
<p>The shadow root pulls any user provided content into <code>div.inner</code> via the <code>&lt;content&gt;</code> insertion point.</p>
<pre><code class="html">&lt;div class="outer"&gt;
  &lt;div class="inner"&gt;
  &lt;content&gt;&lt;/content&gt;
  &lt;/div&gt;
&lt;/div&gt;</code></pre>
<p>I also want to create <code>&lt;x-dialog-alert&gt;</code> that looks and behaves just like <code>&lt;x-dialog&gt;</code> but with a more restricted API, a bit like <code>alert('foo')</code>.</p>
<pre><code class="html">&lt;x-dialog-alert&gt;foo&lt;/x-dialog-alert&gt;</code></pre>
<pre><code class="js">var proto = Object.create(XDialog.prototype);

proto.createdCallback = function() {
  XDialog.prototype.createdCallback.call(this);
  this.createShadowRoot();
  this.shadowRoot.innerHTML = templateString;
};

document.registerElement('x-dialog-alert', { prototype: proto });
</code></pre>
<p>The new component will have its own shadow root, but it&#8217;s designed to work <em>on top</em> of the parent class&#8217;s shadow root. The <code>&lt;shadow&gt;</code> represents the &#8216;older&#8217; shadow root and allows us to project content inside it.</p>
<pre><code class="html">&lt;shadow&gt;
  &lt;h1&gt;Alert&lt;/h1&gt;
  &lt;content&gt;&lt;/content&gt;
  &lt;button&gt;OK&lt;/button&gt;
&lt;/shadow&gt;</code></pre>
<p>Once you get your head round multiple shadow roots, they become a powerful concept. The downside is they bring a lot of complexity and introduce a lot of edge cases.</p>
<h4>Inheritance without multiple shadows</h4>
<p>Inheritance is still possible without multiple shadow roots, but it involves manually mutating the super class&#8217;s shadow root.</p>
<pre><code class="js">
var proto = Object.create(XDialog.prototype);

proto.createdCallback = function() {
  XDialog.prototype.createdCallback.call(this);
  var inner = this.shadowRoot.querySelector('.inner');

  var h1 = document.createElement('h1');
  h1.textContent = 'Alert';
  inner.insertBefore(h1, inner.children[0]);

  var button = document.createElement('button');
  button.textContent = 'OK';
  inner.appendChild(button);

  ...
};

document.registerElement('x-dialog-alert', { prototype: proto });
</code></pre>
<p><strong>The downsides of this approach are:</strong></p>
<ol>
<li>Not as elegant.</li>
<li>Your sub-component is dependent on the implementation details of the super-component.</li>
<li>This wouldn&#8217;t be possible if the super component&#8217;s shadow root was &#8216;closed&#8217;, as <code>this.shadowRoot</code> would be <code>undefined</code>.</li>
</ol>
<h2>HTML Imports</h2>
<p>HTML Imports provide a way to import all assets defined in one <code>.html</code> document, into the scope of another.</p>
<pre class="html"><code><span class="tag">&lt;link</span> <span class="atn">rel</span><span class="pun">=</span><span class="atv">"import"</span> <span class="atn">href</span><span class="pun">=</span><span class="atv">"/path/to/imports/stuff.html"</span><span class="tag">&gt;</span></code></pre>
<p>As <a href="https://hacks.mozilla.org/2014/12/mozilla-and-web-components/">previously stated,</a> <em>Mozilla</em> is not <em>currently</em> intending to implementing HTML Imports. This is in part because we&#8217;d like to see how ES6 modules pan out before shipping another way of importing external assets, and partly because we don&#8217;t feel they enable much that isn&#8217;t already possible.</p>
<p>We&#8217;ve been working with Web Components in <em>Firefox OS</em> for over a year and have found using existing module syntax (AMD or Common JS) to resolve a dependency tree, registering elements, loaded using a normal <code>&lt;script&gt;</code> tag seems to be enough to get stuff done.</p>
<p>HTML Imports do lend themselves well to a simpler/more declarative workflow, such as the older <a href="https://github.com/MikeMitterer/DART-Sample-PolymerElementConsumer/blob/master/web/poly/components/CustomElements/workbench/HTMLElementElement.html"><code>&lt;element&gt;</code></a> and <a href="https://www.polymer-project.org/0.8/docs/migration.html#registration"><em>Polymer&#8217;s </em>current</a> registration syntax.</p>
<p>With this simplicity has come <a href="http://tjvantoll.com/2014/08/12/the-problem-with-using-html-imports-for-dependency-management/">criticism</a> from the community that Imports don&#8217;t offer enough control to be taken seriously as a dependency management solution.</p>
<p>Before the decision was made a few months ago, <em>Mozilla</em> had a working implementation behind a flag, but struggled through an incomplete specification.</p>
<h3>What will happen to them?</h3>
<p><em>Apple&#8217;s </em><a href="https://github.com/w3c/webcomponents/wiki/Isolated-Imports-Proposal">Isolated Custom Elements</a> proposal makes use of an HTML Imports style approach to provide custom elements with their own document scope;: Perhaps there&#8217;s a future there.</p>
<p>At <em>Mozilla</em> we want to explore how importing custom element definitions can align with upcoming ES6 module APIs. We&#8217;d be prepared to implement if/when they appear to enable developers to do stuff they can&#8217;t already do.</p>
<h2>To conclude</h2>
<p>Web Components are a prime example of how difficult it is to get large features into the browser today. Every API added lives indefinitely and remains as an obstacle to the next.</p>
<p>Comparable to picking apart a huge knotted ball of string, adding a bit more, then tangling it back up again. This knot, our platform, grows ever larger and more complex.</p>
<p>Web Components have been in planning for over three years, but we&#8217;re optimistic <strong>the end is near</strong>. All major vendors are on board, enthusiastic, and investing significant time to help resolve the remaining issues.</p>
<p><strong>Let&#8217;s get ready to componentize the web!</strong></p>
<h4>More</h4>
<ul>
<li>Join the ongoing discussion on the <a href="https://lists.w3.org/Archives/Public/public-webapps/">public-webapps mailing list</a>.</li>
<li>Keep an eye on the <a href="https://github.com/w3c/webcomponents/">W3C Web Components Repo</a>.</li>
<li>Sign-up to the <a href="http://webcomponentsweekly.me/">Web Components Weekly</a> newsletter.</li>
<li>Play with Web Components in Firefox today by turning on &#8216;dom.webcomponents.enabled&#8217; pref in about:config.</li>
</ul>
