Editor’s note: This is part one of a two-part series by Microsoft Edge engineers Travis Leithead and Arron Eicholz. Part two will be coming tomorrow, July 15th.
Four of our five most-requested platform features on UserVoice (Shadow DOM, Template, Custom Elements, HTML Imports) belong to the family of features called Web Components. In this post we’ll talk about Web Components and give our viewpoint, some background for those who may not be intimately familiar with them, and speculate a bit about where we might expect them to evolve in the future. To do it justice requires a bit of length, so sit back, grab a coffee (or non-caffeinated beverage) and read-on. In part two (coming tomorrow), we’ll address questions about our roadmap and plans for implementation.
Table of Contents:
- Componentization: An old design practice made new again for the Web.
- How to componentize?
- Not the first time: Past approaches to componentization
- Today’s Web Components
- Web Components: The Next Generation
Componentization: an old design practice made new again for the web
Web applications are now as complex as any other software applications and often take many people coordinating to produce the released product. It is essential to find the right way to divide up the development work with minimal overlap between people and systems in order to be more efficient. Componentization (in general) is how this is done. Any component system should reduce overall complexity by providing isolation, or a natural barrier that hides the complexity of one system from another. Good isolation also makes reusability and serviceability easier.
Initially, web application complexity was managed mostly on the server by isolating the application into separate pages, each requiring the user to navigate their browser from page to page. With the introduction of AJAX and related capabilities, developers no longer needed to “navigate” between different pages of a web application. For some common scenarios like reading email or news, expectations have changed. For example, after logging into your email, you may be “running the email application” from a single URL and stay on that page all day long (aka Single-Page Applications). Client-side web application logic may be much more complex, perhaps even rivaling that of the server side. A possible solution to help solve this complexity is to further componentize and isolate logic within a single web page or document.
The goal of web components is to reduce complexity by isolating a related group of HTML, CSS, and JavaScript to perform a common function within the context of a single page.
How to componentize?
Because web components must draw together each of HTML, CSS, and JavaScript, the existing isolation models supported by each technology contribute to scenarios that are important to preserve in the whole of web components. These independent isolation models include (and are described in more detail in the following paragraphs):
- CSS Style isolation
- JavaScript and scopes (closures)
- Global object isolation
- Element encapsulation (the iframe)
CSS style isolation
There is no great way to componentize CSS natively in the platform today (though tools like Sass can certainly help). A component model must support a way to isolate some set of CSS from another such that the rules defined in one don’t interfere with the other. Additionally, component styles should apply only to the necessary parts of the component and nothing else. Easier said than done!
Within a style sheet, CSS styles are applied to the document using Selectors. Selectors are always considered with potential applicability to the whole document, thus their reach is essentially global. This global reach leads to real conflicts when many contributors to a project pool their CSS files together. Overlapping and duplicate selectors have an established precedence (e.g., cascade, specificity and source-order) for resolving conflicts, but the emergent behavior may not be what the developers intended. There are many potential solutions to this problem. A simple solution is to move elements and related styles participating in a component from the primary document to a different document (a shadow document) such that they are no longer selector matching candidates. This gives rise to a secondary problem: now that there is a boundary established, how does one style across the boundary? This is obviously possible to do imperatively in JavaScript, but it seems awkward to rely on JavaScript to mediate styles over the boundary for what seems like a gap in CSS.
To transmit styles across a component boundary effectively, and to protect the structure of a component (e.g., allow freedom of structural changes without breaking styles), there are two general approaches that have some consensus: “parts” styling using custom pseudo elements and custom properties (formerly known as CSS “variables”). For a time, the ultra-powerful cross-boundary selector combinator‘>>>’ was also considered (specified in CSS Scoping), but this is now generally accepted as a bad idea because it breaks component isolation too easily.
Parts styling would allow component authors to create custom pseudo elements for styling, thereby exposing only part of their internal structure to the outside world. This is similar to the model that browsers use to expose the “parts” of their native controls. To complete the scenario, authors would likely need some way to restrict the set of styles that could apply to the pseudo element. Additional exploration into a pseudo-element-based “parts model” could result in a useful styling primitive, though the details would need to be ironed out. Further work in a parts model should also rationalize browser built-in native control styling (an area that desperately needs attention).
Custom properties allow authors to describe the style values they would like to re-use in a stylesheet (defined as custom property names prefixed by a double-dash). Custom properties inherit through the document’s sub-tree, allowing selectors to overwrite the value of a custom property for a given sub-tree without affecting other sub-trees. Custom property names would also be able to inherit across component boundaries providing an elegant styling mechanism for components that avoids revealing the structural nature of the component. Custom properties have been evaluated by various Google component frameworks, and are reported to address most styling needs.
Of all the styling approaches considered so far, a future “parts” model and the current custom properties spec appear to have the most positive momentum. We consider custom properties as a new essential member of the web components family of specifications.
Other CSS Style isolation approaches
By way of completeness, scoping and isolation of CSS is not quite as black-and-white as may be assumed above. In fact, several past and current proposals offer scoping and isolation benefits with varying applicability to web components.
CSS provides some limited forms of Selector isolation for specific scenarios. For example, the @media rule groups a set of selectors together and conditionally applies them when the media conditions are met (such as size/dimension of the viewport, or media type–e.g., printing); the @page rule defines some styles that are only applicable to printing conditions (paged media); the @supports rule collects selectors together to apply only when an implementation supports a specific CSS feature–the new form of CSS feature-detection); the proposal for @document groups selectors together to be applied only when the document in which the style sheet is loaded matches the rule.
The CSS Scoping feature (initially formed as part of the web components work) is a proposal for limiting CSS selector applicability within a single HTML document. It defines a new rule @scope which enables a selector to identify scoping root(s) and then causes the evaluation of all selectors contained within the @scope rule to only have subtree-wide applicability to that root (rather than document-wide applicability). The specification allows for HTML to declaratively define a scoping root (e.g., the proposed
It’s important to note that @scope only establishes a one-way isolation boundary: selectors contained within the @scope are constrained to the scope, while any other selectors (outside the @scope) are still free to select inside the @scope at will (though they may be ordered differently by the cascade). This is an unfortunate design because it does not offer scoping/isolation to any of the selectors that are not in the @scope subset–all CSS must still “play nice” in order to avoid accidental styling within another’s @scope rule. See Tab’s @in-shadow-of sketch that is better aligned with a model for protecting component isolation.
Another proposed form of scoping is CSS Containment. Containment scoping is less about Style/Selector isolation and more about “layout” isolation. With the “contain” property, the behavior of certain CSS features that have a natural inheritance (in terms of applicability from parent to child element in the document, e.g., counters) would be blocked. The primary use case is for developers to indicate that certain elements have a strong containment promise, such that the layout applicable to that element and its sub-tree will never effect the layout of another part of the document. These containment promises (enforced by using the ‘contain’ property) allow browsers to optimize layout and rendering such that a “dirty” layout in the contained sub-tree would only require that sub-tree’s layout to be updated, rather than the whole document.
As the implementations of web component technologies across browser vendors mature and get more and more public use, additional styling patterns and problems may arise; we should expect to see further investment and more progress made on various CSS proposals to improve web component styling as a result.
JavaScript and scopes
All JavaScript that gets included into a web page has access to the same shared global object. Like any programming language, JavaScript has scopes that provide a degree of “privacy” for a function’s code. These lexical scopes are used to isolate variables and functions from the rest of the global environment. The JavaScript “module pattern” in vogue today (which uses lexical scopes) evolved out of a need for multiple JavaScript frameworks to “live together” within the single global environment without “stomping” over each other (depending on load-order).
Lexical scopes in JavaScript are a one-way isolation boundary–code within a scope can access both the scope’s contents as well as any ancestor scopes up to the global scope, while code outside of the scope cannot access the scope’s contents. The important principle is that the one-way isolation favors the code inside the scope, protecting it. The code in the lexical scope has the choice to protect/hide its code from the rest of the environment (or not).
The contribution that JavaScript’s lexical scopes lend to web components is the requirement to have a way of “closing” a component off such that its contents can be made reasonably private.
Global object isolation
Some code may not want to share access to the global environment as described above. For example, some JavaScript code may not be trusted by the application developer–yet it provides a crucial value. Ads and ad frameworks are such examples. For security assurance in JavaScript, it is required to run untrusted code in a separate, clean, scripting environment (one with its own unique global object). Developers may also prefer a fresh global object in which to code without concern for other scripts. In order to do that today (without resorting to iframe elements) developers can use a Worker. The downside to Workers is that they do not provide access to elements, and hence UI.
There are a number of considerations when designing a component that supports global object isolation–especially if that isolation will enable a security boundary (more on that just below). Isolated components are not expected to be fully developed until after the initial set of web components specifications are locked down (i.e., “saved for v2″). However, spending some time now to look forward to what isolated components may be like will help inform some of the work going on today. Severalproposals have been suggested and are worth looking into.
Global object isolation fills an important missing scenario for web components. In the mean-time, we must rely on today’s most successful and widely-deployed form of componentization on the web today: the iframe element.
Element encapsulation (the iframe)
Iframe elements and their related cousins: object elements, framesets, and the imperative window.open() API already provide the ability to host an isolated element subtree. Unlike components which are intended to run inside of a single document, iframes join whole HTML documents together; as if two separate web applications are being co-located, one inside the other. Each has a unique document URL, global scripting environment, and CSS scope; each document is completely isolated from the other.
iframes are the most successful (and only widely-deployed) form of componentization on the web today. Iframes enable different web applications to collaborate. For example, many websites use iframes as a form of component to enable everything from advertisements to federated identity login. Iframes have a set of challenges and ways in which those challenges have been addressed:
- JavaScript code within one HTML document may potentially breach into another document’s isolation boundary (e.g., via the iframe’s contentWindow property). This ability to breach the iframe’s isolation boundary may be a required feature, but is also a security risk when the content in the iframe contains sensitive information not intended for sharing. Today, unwanted breaching is mitigated by the same-origin policy: document URL’s from the same origin are allowed to breach by default, while cross-origin documents have only limited ability to communicate with each other.
- Breaching alone is not the only security risk. The use of a provides further restrictions on the cross-origin iframe in order to protect the host from unwanted scripting, popups, navigation, and other capabilities otherwise available in the frame.
- CSS styles from outside the framed document are unable to apply to the document within. This design preserves the principle of isolation. However, style isolation creates a significant seam in the integration of the iframe when using it as a component (within the same-origin). HTML addresses this with the proposed for same-origin iframes. The use of the seamless attribute removes style isolation from the framed content; seamless framed documents take a copy of their host document’s styles, and are rendered as if free of the confines of their hosted frame element.
With good security policies and the seamless frame feature, using the iframe as a component model appears to be a pretty attractive solution. However, a number of desirable properties of a web component model are lacking:
- Deep integration. Iframes limit (and mostly stop completely) integration and interaction models between the host and framed document. For example, relative to the host, focus and selection models are independent and event propagation is isolated to one or the other document. For components that want to more closely integrate, supporting these behaviors is impossible without an “agent” composed in the host’s document to facilitate bridging the boundary.
- Proliferation of global objects. For each iframe instance created on a page there will always be a unique global object created as well. Global objects and their associated complete type system are not cheap to create and can end up consuming a lot of memory and additional overhead in the browser. Multiple copies of the same component used on a page may not need to each be fully isolated from each other, in fact sharing one global object is preferable especially where common shared state is desired.
- Host content model distribution. Iframes currently do not allow re-use of the host element’s content model within the framed document. (Simply: an element’s content model is its supported sub-tree of elements and text.) For example, a select element has a content model that includes option elements. A select element implemented as a web component would also want to consume those same children in like manner.
- Selective styling. The seamless iframe does not work for cross-origin documents. There are subtle security risks if this was allowed to happen. The main problem is that “seamless” is controlled by the host, not the framed document (the framed document is more often the victim in related attacks). For a component, the binary “seamless” feature may be too extreme; components may want to be more selective in deciding which styles from the host should be applicable to its content (rather than automatically inheriting all of them i.e., how seamless works). In general, the question of what to style should belong to the component.
- API exposure. Many scenarios for web components involve the creation of full-featured new “custom” elements with their own exposed API surface, rendering semantics, and lifecycle management. Using iframes limits the developer to working with the iframe’s API surface as a baseline, with all the assumptions that brings with it. For example, the identity of the element as well as its lifecycle semantics cannot be altered.
Not the first time
It’s worth noting that several past technologies have been proposed and implemented in an attempt to improve upon HTML’s iframe and related encapsulation features. None of these has survived in any meaningful way on the public web today:
- HTML Components (1998) was proposed and implemented by Microsoft starting in IE5.5 (obsoleted in IE10). It used a declarative model for attaching events and APIs to a host element (with isolation in mind) and parsed components into a “viewlink” (a “Shadow DOM”). Two flavors of components were possible, one permanently attached to an element, and another dynamically bound via a CSS “behavior” property.
- XBL (2001) and its successor XBL2 (2007) was proposed by Mozilla as a companion to their XUL user-interface language. A declarative binding language with two binding flavors (similar to Microsoft’s HTML Components), XBL supported the additional features of host content model distribution and content generation.
Today’s web components
After the two previous failures to launch, it was time again for another round of component proposals, this time led by Google. With the concepts described in XBL as a starting point, the monolithic component system was broken up into a collection of component building blocks. These building blocks allowed web developers to experiment with some useful independent features before the whole of the web components vision was fully realized. It was this componentization and development of independently useful features that has contributed to its success. Nearly everyone can find some part of web components useful for their application!
This new breed of web components aspire to meet a set of defined use-cases and do so by explaining how existing built-in elements work in the web platform today. In theory, web components allow developers to prototype new kinds of HTML elements with the same fidelity and characteristics as native elements (in practice the accessibility behaviors in HTML are especially hard to match at the moment).
It is clear that the full set of technologies necessary to deliver upon all the web components use-cases, will not all be available in browsers at first. Implementors are working together to agree upon the core set of technologies whose details can be implemented consistently before moving on to additional use-cases.
The “first generation” of web components technologies are:
- Custom elements: Custom elements define an extension point for the HTML parser to be able to recognize a new “custom element” name and provide it with a JavaScript-backed object model automatically. Custom elements do not enable a component boundary, but rather provide the means for the browser to attach an API and behavior to an author-defined element. Un-supported browsers can polyfill Custom elements to varying levels of precision using mutation observers/events and prototype adjustment. Getting the timing right and understanding the implication is one of the subjects of our upcoming meeting.
- “is” attribute. Hidden inside the Custom Elements spec is another significant feature–the ability to indicate that a built-in element should be given a custom element name and API capabilities. In the normal case, the custom element starts as a generic element; with “is”, a native element can be used instead (e.g., ). While this feature is a great way to inherit all the goodness of default rendering, accessibility, parsing, etc., that is built-in to specific HTML elements, its syntax is somewhat regarded as a hack and others wonder if accessibility primitives plus native control styling are a better long-term standardization route.
- Shadow DOM: Provides an imperative API for creating a separate tree of elements that can be connected (only once) to a host element. These “shadow” children replace the “real” children when rendering the document. Shadow DOM also provides re-use of the host element’s content model using new slot elements (recently proposed), event target fixup, and closed/open modes of operation (also newly adopted). This relatively trivial idea has a surprisingly huge number of side-effects on everything from focus models and selection to composition and distribution (for shadow DOMs inside of shadow DOMs).
- CSS Scoping defines various pseudo elements relevant for shadow DOM styling, including :host, ::content (soon-to-be ::slot??), and the former ‘>>>’ (shadow dom piercing combinator) which is now officially disavowed.
- The template element: Included for completeness, this early web components feature is now part of the HTML5 recommendation. The template element introduced the concept of inertness (template’s children don’t trigger downloads or respond to user input, etc.) and was the first way to declaratively create a disconnected element subtree in HTML. Template may be used for a variety of things from template-stamping and data-binding to conveying the content of a shadow DOM.
- HTML Imports: Defines a declarative syntax to “import” (request, download and parse) HTML into a document. Imports (using a link element’s rel=”import”) execute the imported document’s script in the context of the host page (thus having access to the same global object and state). The HTML, JavaScript, and CSS parts of a web component can be conveniently deployed using a single import.
- Custom Properties: Described in more detail above, custom properties described outside of a component that are available for use inside the component are a simple and useful model for styling components today. Given this, we include custom properties as part of the first generation of web components technologies.
Web Components: The Next Generation
As noted at the start of this post, building out the functionality for web components is a journey. A number of ideas to expand and fill gaps in the current generation of features have already started to circulate (this is not a complete index):
- Declarative shadow DOM. A declarative shadow DOM becomes important when considering how to re-convey a component in serialized form. Without a declarative form, techniques like innerHTML or XMLSerializer can’t build a string-representation of the DOM that includes any shadow content. Shadow DOMs are thus not round-tripable without help from script. Anne from Mozilla proposed a
element as a strawman proposal. Similarly, the template element is already a declarative approach to building a form of shadow markup, and serialization techniques in the browser have already been adjusted to account for this and serialize the template “shadow” content accordingly. - Fully Isolated Components. Threebrowservendors have made three different proposals in this space. These three proposals are fairly well aligned already which is good news from a consensus-building standpoint. As mentioned previously, isolated components would use a new global object, and might be imported from cross-origin. They would also have a reasonable model for surfacing an API and related behavior across their isolation boundary.
- Accessibility primitives. Many in the accessibility community like the idea of “is” (from Custom Elements) as a way of extending existing native elements because the native elements carry accessibility behaviors that are not generally available to JavaScript developers. Ideally, a generic web component (without using “is”) could integrate just as closely as native elements in aspects of accessibility, including form submission and focus-ability among others; these extension points are not possible today, but should be explored and defined.
- Unified native control styling. Lack of consistent control styling between browsers has been an interop problem area preventing simple “theming-oriented” extensions from being widely deployed. This leads developers to often consider shadow DOM as a replacement solution (though mocking up a shadow DOM with the same behavioral fidelity as a native control can be challenging). Some control-styling ideas have been brewing in CSS for some time, though with lack of momentum these ideas are not being developed very quickly.
- CSS Parts styling. While CSS custom properties can deliver a wide range of styling options for web components today, there are additional scenarios where exposing some of the component’s encapsulated boxes for styling is more direct or appropriate. This is especially useful if it also rationalizes the approach that existing browsers use to expose parts styling of native controls.
- Parser customization. When used with Custom Elements, only standard open/close tags may be used for custom element parsing. It may be desirable to allow additional customization in the parser for web components to take advantage of.
Finally, while not officially considered part of web components, the venerable iframe should not be neglected. As discussed, iframes are still a highly useful feature of the web platform for building component-like things. It would be useful to understand and potentially improve the “component” story for iframes. For example, understanding and addressing the unresolved problems with
Web Components are a transformative step forward for the web. We are excited to continue to support and contribute to this journey. Tomorrow, we’ll share more about our roadmap and plans for implementation. In the meantime, please share your feedback @MSEdgeDev on Twitter or in the comments below.
– Travis Leithead, Program Manager, Microsoft Edge
– Arron Eicholz, Program Manager, Microsoft Edge