Over a million developers have joined DZone.
{{announcement.body}}
{{announcement.title}}
Refcard #253

Introduction to Web Components

Download this new Refcard to learn more about Web Components. Newly named Java Champion Kito Mann walks you through the process from set up to using helpful libraries.

7,961
Free .PDF for easy Reference

Written by

Kito Mann Principal Consultant, Virtua, Inc.
Refcard #253

Introduction to Web Components

Download this new Refcard to learn more about Web Components. Newly named Java Champion Kito Mann walks you through the process from set up to using helpful libraries.

7,961
Free .PDF for easy Reference

Written by

Kito Mann Principal Consultant, Virtua, Inc.
Table of Contents

What is a Web Component?

Key Specifications

Related Specifications

Browser Support

Using Custom Elements

Libraries to Simplify your Life

Testing

Playing with Others

Where Next?

Section 1

What is a Web Component?

It’s no secret that it is easier to build a user interface out of reusable components. UI components allow developers to focus on the application logic rather than spend time reinventing a data table or drop-down list box. They also allow teams to build specialized UI components that can be re-used in different projects, or even across an entire organization.

In fact, component models has been popular since the early 1990s with technologies such as Visual Basic, Delphi, PowerBuilder, WinForms, Windows Presentation Framework, ASP.NET, Swing, JavaFX, JavaServer Faces, Tapestry, and so on. iOS and Android have their own component models. In the browser, we’ve had plethora of component models over the years, including YUI, KendoUI, Bootstrap, jQuery UI, and Wijmo. Modern front-end frameworks such React, Angular, and Vue each have their own compponent models and enforce a component-centric development model.

However, the browser itself has never had a native UI component model. Each framework has had to create their own out of customized CSS, JavaScript, and a bunch of HTML elements like <div>, <span>, and <ul>. Even with modern frameworks, ultimately the browser is working with these primitives.

A Native Component Model for HTML

Web Components are a set of Web Platform APIs that allow you to create new custom, reusable, encapsulated HTML tags to use in web pages and web apps. They work with modern browsers and can be used with any JavaScript library or framework that works with HTML.

For example, let’s say you need a dialog box:

Image title

This can be implemented as a Web Component, and used within an HTML page like so:

<paper-action-dialog backdrop autoCloseDisabled layered="false">
  <p>Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.</p>

  <paper-button affirmative autofocus>Tap me to close</paper-button>
</paper-action-dialog>

The <paper-action-dialog> and <paper-button> tags are fully encapsulated, with their own properties, methods, and events. No framework required.

In order to get a sense of over 1,000 publically available Web Components, see webcomponents.org.

Section 2

Key Specifications

Web Components are made up of four key specifications:

Specification Description Status
Custom Elements Use JavaScript to write new elements that act like regular HTML elements in the browser. Working Draft
HTML Template Declare fragments of HTML which aren’t used at when the page is loaded, but can be used later. Used to manage the internal markup of a custom element. HTML5 Living Standard
Shadow DOM Create DOM trees that are hidden from gobal CSS styles and DOM queries. Allows you to hide the implementation details of a custom element. Working Draft
HTML Imports Import other HTML documents, each of which can reference JavaScript, CSS, etc. Used for managing dependencies. Working Draft
Section 3

Related Specifications

The following specifications aren’t officially part of Web Components, but are often used with them:

Specification Description Status
Custom Event Create new DOM events that can have specialized data. Allows custom elements to generate their own events. HTML5 Living Standard
CSS Custom Properties Declare CSS variables that can be reused in different styles. Candidate Recommendation
CSS Apply Rule Group a set of CSS properties together as a variable that can be referenced in other styles values. Also called CSS ‘mixins’. Unofficial Proposal

CSS Custom Properties and CSS Apply Rule provide much of the power of CSS pre-processors such as SASS natively in the browser.

Section 4

Browser Support

Web standards are great, but they’re not very useful if browsers don’t implement them. Even if something is an official standard, it’s up to the browser vendors to decide whether or not to implement them, and when. The following table shows the current browser support for these specifications as of this writing, with links to , which provides up-to-date browser support info plus additional details. The table refers to the latest version of the browser (with the exception of IE11).

Specification Chrome Safari IE 11 Edge Firefox Link to Details
Custom Elements Supported Supported Not Supported Under Consideration In Development http://caniuse.com/#search=custom%20elements%20v1
HTML Template Supported Supported Not Supported Supported In Development http://caniuse.com/#search=HTML%20Template
Shadow DOM Supported Supported Not Supported Under Consideration In Development http://caniuse.com/#search=shadow%20dom%20v1
HTML Imports Supported Not Supported Not Supported Under Consideration Not Supported http://caniuse.com/#search=html%20imports
Custom Event Supported Supported Partially Supported Supported Supported http://caniuse.com/#search=custom%20event
CSS Custom Properties Supported Supported Not Supported Supported Supported http://caniuse.com/#search=custom%20properties

IE 11 isn’t under active development, so it’s no surprise that it doesn’t support these specs. There is broad consensus among browser vendors for the Custom Elements, HTML Template, and Shadow DOM (Custom Event and CSS Custom Properties have been around longer and are well supported). There are two outliers: HTML Imports and CSS Apply Rule. Unfortunately, HTML Imports has been rejected by browser vendors other than Google because it represents a dependency model that competes with JavaScript (ES6) modules. There is an ongoing discussion about bringing back something similar that plays well with ES6 Modules, called HTML Modules. As a consequence, projects that rely on them (such as Polymer) are switching to ES6 Modules in the future. We'll also skip coverage of HTML Imports in this Refcard.

When a browser doesn’t implement a standard, support can be added with a polyfill. The most popular Web Components polyfill is webcomponents.js. It supports all of the specifications listed above, and has the ability to only load polyfills for a specification if the feature is not supported in the browser. It is battle-tested, performant, and in production on many public and private apps, including the new desktop version of YouTube.

Section 5

Using Custom Elements

A Custom Element is simply a JavaScript ES6 class that sublcasses HTMLEmlement (the base class used by all of the native HTML elements). After the class has been defined, the element must be registered by calling customElements.define(). (If you need to support browsers like IE11 that don’t support ES6 classes, you will need to transpile to ES5 using a transpiler such as Babel and also include custom-elements-es5-adapter from webcomponents.js.)

Once it has been registered with the document with customElements.define(), you can then start using it in your page (assuming you have loaded the webcomponents.js polyfill and the JavaScript for the custom element itself).

Element Upgrades

The browser progressively enhances custom elements by default. When the element is initilaly parsed, it is a plain HTMLElement until customElements.define() is called (this process is called “element upgrades”). This means that users can work with the element before it is upgraded; keep this in mind when writing your elements. You can check to see if an element has been upgraded by calling customElements.whenDefined(), which returns a Promise:

customElements.whenDefined('vt-counter').then(() => {
  console.log('vt-counter defined');
});

Lifecycle Callbacks

Custom Elements can optionally implement several callbacks that get executed during their lifecycle.

Callback Description
constructor() Called when the element is upgraded or created using document.createElement() or using the new keyword. Useful for initializing the Shadow DOM and performing any other initialization work that doesn’t involve attributes or “light DOM” children (child elements which are not part of the Shadow DOM).
connectedCallback() Called when the element has been added to the DOM. Useful for initializing properties/attributes, fetching resources, light DOM children, etc. Most of the setup work should be performed here.
attributeChangedCallback(attrName, oldVal, newVal) Called whenever an attribute value has been added, removed, or changed. Also called for the initial attribute values when the element is upgraded. Useful for handling any behaviorial side-effects of changes to attribute values. Note this is only called for attributes defined using the observedAttributes() static property (otherwise you would get a call everytime someone updated default attributes, like class or id).
disconnectedCallback() Called when the element is removed from the DOM (someone called remove() on the element); useful for cleanup (such as removing event listeners). Note that this callback won’t always be called (for example if someone closes the tab).
adoptedCallback() Called when an element is moved to another document. This happens when someone calls document.adoptNode(). This is not supported by the webcomponents.js polyfill.

Note that these callbacks are synchronous, so make sure they are fast.

Properties vs Attributes

All HTML elements have an array of attributes which are strings. For example, given the element my-widget below:

    <my-widget id="myWidget" type="round" weight="45"></my-widget>

The attributes are id, type, and weight. They can be set programmatically like so:

    const widget = document.getElementById('myWidget');
    widget.setAttribute('type', 'square');

However, we often want to use actual properties, which allow us to use a getter and setter, as well as a nicer programmatic interface:

    widget.type = 'square';

In this case, we need to synchronize the property and attribute values. When the act of updating a property also updates the corresponding attribute, this is considered “reflecting to the attribute”. Only do this for primitive types; if the property is an Object or Array, you’re better off storing it as a normal property on your custom element (serializing large objects to the DOM as an attribute is expensive).

Examples

Here’s an example of a custom element that counts up from first, or 0 if first is not defined. The user can start and stop the timer by clicking on it. Let’s start with the first few methods:

class VirtuaTrainingCounter extends HTMLElement {
    static get observedAttributes() {
        return ['value', 'interval'];
    }
    // Store properties as attributes so they work both ways.
    get interval() {
        const attr = this.getAttribute('interval');
        return attr ? Number(attr) : 2000;
    }
    set interval(interval) {
        this.setAttribute('interval', interval);
        this.stop();
        this.start();
    }
    // Don't store this property as an attribute because it changes frequently.
    get value() {
        if (!this._value) {
            this._value = 0;
        }
        return this._value;
    }
    set value(value) {
        const num = Number(value);
        this._value = num;
        if (this._content) {
            this._content.innerText = num;
        }
    }
    constructor() {
        super();
        console.log('inside constructor');
        this._onClick = this._onClick.bind(this);
    }
    /** Fires after an instance has been inserted into the document */
    connectedCallback() {
        console.log('inside connectedCallback');
        this._content = document.createElement('span');
        this._content.innerText = 'Counter';
        this._content.className = 'counter-disabled';
        this.appendChild(this._content);
        this._upgradeProperty('value');
        this._upgradeProperty('interval');
        this.addEventListener('click', this._onClick);
    }
...
// Registers <vt-counter> as a custom element
window.customElements.define('vt-counter', VirtuaTrainingCounter);

So far, there isn’t much going on here. The static observedAttributes property tells the browser to only call attributeChangedCallback() when these attributes are set.

In connectedCallback(), we setup the light DOM of the component, register the event handler for click events, and initialize the properties. The call to upgradeProperty() is of particular interest. Using a method like this isn’t a requirement, but it helps to ensure that your custom element works in frameworks (such as Angular) that may set the property before the component is upgraded. In this case, the property would not have gone through the setter, so we need to perform a little trickery: delete the property and then re-add it (which ensures that the setter is called):

    _upgradeProperty(prop) {
        if (this.hasOwnProperty(prop)) {
            let value = this[prop];
            delete this[prop];
            this[prop] = value;
        }
    }

The connectedCallback() method also registers the _onClick() method, which starts or stops the timer when the user clicks on it.

_onClick() {
if (!this._timer) {
this.start();
}
else{
this.stop();
}
}

The start()   and stop()   methods perform the real work of the component:

start() {
    this._content.classList.remove('counter-disabled');
    this._content.innerText = this.value;
    this._timer = setInterval(() => {
        this.value++;
    }, this.interval);
    this.dispatchEvent(new CustomEvent('vt-counter-started', {
        detail: {
            value: this.value,
        },
        bubbles: true,
        cancelable: true,
        composed: true
    }));
}
stop() {
    if (this._timer) {
        clearInterval(this._timer);
        this._timer = null;
        this._content.classList.add('counter-disabled');
        this.dispatchEvent(new CustomEvent('vt-counter-stopped', {
            detail: {
                value: this.value
            },
            bubbles: true,
            cancelable: true,
            composed: true
        }));
    }
}

In the start() method, we remove the counter-disabled class so it’s clear the timer is running and use JavaScript’s setTimeout() function to increment the value property every interval  milliseconds. We also fire a custom event to let users know the timer has started. (The  composed value is important; it ensures the event will bubble out of the shadow DOM if the counter is used inside of a shadow root). You may have noticed the name of the custom event has a prefix of “vt-counter”. This helps avoid confusion that can occur if multiple components emit an event with the same name.

In the stop() method, we add the ‘counter-disabled’ class, so it’s clear the timer has stopped, and remove the timer. This time, we fire a different custom event to inform users that the timer has stopped.

When someone sets value attribute, we want to update the value property. This will allow us to set the initial value declaratively in HTML, but will not update the attribute in the DOM every time the property changes. We can achieve this by using the attributeChangedCallback():

/**
 * Fires after an attribute has been added, removed, or updated. Updates `value` property if the `value` attribute
 * is updated.
 */
attributeChangedCallback(attr, oldVal, newVal) {
    console.log('inside attributeChangedCallback', 'attr:', attr, 'oldVal:', oldVal, 'newVal:', newVal);
    if (attr === 'value') {
        this.value = newVal;
    }
}

Finally, we need to do some cleanup work in the disconnectedCallback():

/**
 * Fires after an instance has been removed from the document. Here
 * we stop the timer and remove event listeners.
 */
disconnectedCallback() {
    console.log('inside disconnectedCallback');
    this.stop();
    this.removeEventListener('click', this._onClick);
};

Here we stop the timer and remove the event listener.

Once you’ve written a custom element, you can include it just like any other JavaScript code, and then use it inside of your HTML page:

<!DOCTYPE html>
<html>
<head>
    <meta charset="UTF-8">
    <title>Counter Web Component Demo</title>

    <!-- Importing Web Component's Polyfill -->
    <script src="bower_components/webcomponentsjs/webcomponents-loader.js"></script>

    <!-- Import our custom element -->
    <script src="vt-counter.js"></script>

    <style>
        body {
            text-align: center;
        }

        .counter {
            color: indigo;
            font-size: 60pt;
            font-family: sans-serif;
            text-align: center;
            padding: 10px;
            border: 1px black inset;
        }

        .counter:hover {
            cursor: pointer;
        }

        .counter-disabled {
            color: gray;
        }
    </style>
</head>
<body>
    <h1>Counter Web Component Demo</h1>
    <!-- Using our new custom element -->
    <vt-counter class="counter" value="10"></vt-counter>
</body>
</html>

Here we load the webcomponents.js polyfill and our custom element’s source, which allows us to easily use it in the page. We can even style it. Here is a screenshot of the counter after the user has clicked on it:

Image title

View a demo live on Plunker.

Here are a few rules to keep in mind when developing custom elements:

  1. The name must contain a dash (-) so the browser can distinguish between the custom element and regular elements. For example, <foo-bar> and <acme-tabs> are valid names, but <foobar> and <acme_tabs> are not. Most components use a prefix for the project of which they are part, in order to avoid namespace collisions. For example, instead of having a <basic-tabs> component, you might have a <acme-basic-tabs> component if your project was called Acme.
  2. The same name cannot be registered more than once (you will get a DOMException if this happens).
  3. Custom elements cannot be self-closing; you must always use a closing tag (e.g. <my-custom-element></my-custom-element>).

Extending Custom Elements

You can extend custom elements just like any other class (see the following sections for examples). Although the spec mentions subclassing native elements (like <input> or <div>), Apple does not approve of this feature, and no browser currently supports it (see the status). There is currently a discussion about an alternative solution on GitHub.

Using HTML Templates

One of the problems with the Custom Element example in the previous section is that the styles are not self-contained; they must be declared in the document that uses the component. Also, the DOM is built imperatively. Both of these issues can be solved by using HTML templates. Here is a subclass of the VirtuaTrainingCounter component that uses HTML templates:

(function() {

    /**
     * Declare the template here so it can be re-used by multiple instances of the element.
     * Cloning from the template instead of using innerHTML is more performant since the markup
     * is only parsed once.
     */
    const template = document.createElement('template');
    template.innerHTML = `

        <style>
            .counter {
                color: indigo;
                font-size: 60pt;
                font-family: sans-serif;
                text-align: center;
                padding: 10px;
                border: 1px black inset;
            }

            .counter-disabled {
                color: gray;
            }
        </style>

        <span id="value" class="counter counter-disabled">Counter</span>
        `;

    class VirtuaTrainingHtmlTemplateCounter extends VirtuaTrainingCounter {

        /**
         * Fires when an instance was inserted into the document.
         * @override
         */
        connectedCallback() {
            console.log('inside overridden connectedCallback')
            const templateContent = template.content.cloneNode(true);
            this._content = templateContent.getElementById('value');
            this.appendChild(templateContent);

            this._upgradeProperty('value');
this._upgradeProperty('interval');
this.addEventListener('click', this._onClick);
        }
    }


    // Registers <vt-html-template-counter> as a custom element
    window.customElements.define('vt-html-template-counter', VirtuaTrainingHtmlTemplateCounter);
})();

The first difference here is that we include the code inside of an immediately-invoked function expression (IIFE) so that we can declare the template element in a private scope (you could use a module to achieve a similar effect). This allows us to create and initialize the template so that it can be cloned by multiple instances of the component. HTML Templates can also be defined in pure HTML using the <template> element, but this approach ensures the component will work in a larger variety of environments.

Inside of the connectedCallback() (which overrides the version defined in the superclass), we simply clone the template rather than building the DOM programmatically.

The HTML for the page that uses the component is pretty much the same as the previous example, except that we must include the code for the superclass, and there is no need to declare styles for the element:

<!DOCTYPE html>
<html>
<head>
    <meta charset="UTF-8">
    <title>Counter HTML Template Web Component Demo</title>

    <!-- Importing Web Component's Polyfill -->
    <script src="bower_components/webcomponentsjs/webcomponents-loader.js"></script>

    <!-- Import our custom element -->
    <script src="vt-counter.js"></script>
    <script src="vt-counter-html-template.js"></script>

    <style>
        body {
            text-align: center;
        }
    </style>
</head>
<body>
    <h1>Counter HTML Template Web Component Demo</h1>
    <!-- Using our new custom element -->
    <vt-html-template-counter value="10"></vt-html-template-counter>
</body>
</html>

View a demo live on Plunker.

Using Shadow DOM

Even though the styles defined in the previous example are declared inside of the component itself, they’re not truly encapsulated. Any other element on the page can use the CSS classes .counter and .counter-disabled. Moreover, the id of the <span> element isn’t namespaced, so it will collide with any other element on the document with the id value. Both of these issues can be solved with Shadow DOM. All we have to do is create a shadow root for the component and append the cloned template to it. In the following example, VirtuaTrainingShadowDomCounter subclasses VirtuaTrainingCoounter to do just that:

(function() {

    /**
     Declare the template here so it can be re-used by multiple instances of the element.
     Cloning from the template instead of using innerHTML is more performant since the markup
     is only parsed once.
     */
    const template = document.createElement('template');
    template.innerHTML = `
        <style>
            :host {
                border: 1px black inset;
                padding: 10px;
                display: inline-block;
            }
            .counter {
                color: indigo;
                font-size: 60pt;
                font-family: sans-serif;
                text-align: center;
            }
            .counter:hover {
                cursor: pointer;
            }           
            .counter-disabled {
                color: gray;
            }
        </style>

        <slot></slot>
        <span id="value" class="counter counter-disabled"></span>
        `;

    class VirtuaTrainingShadowDomCounter extends VirtuaTrainingCounter {

        /**
         * @override
         */
        constructor() {
            super();

            console.log('inside overridden constructor');
            const templateContent = template.content.cloneNode(true);
            this._content = templateContent.getElementById('value');
            this.attachShadow({mode: 'open'});
            this.shadowRoot.appendChild(templateContent);

        }

        /**
         * Fires when an instance was inserted into the document.
         * @override
         */
        connectedCallback() {
            console.log('inside overridden connectedCallback');
            this._upgradeProperty('value');
this._upgradeProperty('interval');
this.addEventListener('click', this._onClick);
        }
    }

    // Registers <vt-shadow-dom-counter> as a custom element
    window.customElements.define('vt-shadow-dom-counter', VirtuaTrainingShadowDomCounter);
})();

Here, we use an HTML template wrapped in an IIFE as in the previous example, but this time we create a shadow root in the constructor and append the clone of the template there. In the previous example, DOM manipulation was performed in connectedCallback() because the Custom Elements specification disallows creating children in the constructor. The constructor is, however, a fine place to create the shadow root and add children to it. connectedCallback() has been overridden simply to avoid building a light DOM, as the superclass does.

There are a couple of important differences in the template. First, there is the :host CSS pseudo-class; this allows us to style the host of the shadow root, which is the custom element itself. In the markup portion of the template, there is also a new <slot> element. This will insert the content specified inside of the custom element by the calling document.

Usage of this component is the same as the previous example, except we can now include additional content to be displayed inside of the counter:

<!DOCTYPE html>
<html>
<head>
    <meta charset="UTF-8">
    <title>Counter Shadow DOM Web Component Demo</title>

    <!-- Importing Web Component's Polyfill -->
    <script src="bower_components/webcomponentsjs/webcomponents-loader.js"></script>

    <!-- Import our custom element -->
    <script src="vt-counter.js"></script>
    <script src="vt-counter-shadow-dom.js"></script>

    <style>
        body {
            text-align: center;
        }

        .header {
            font-size: 14pt;
            color: black;
        }
    </style>
</head>
<body>
    <h1>Counter Shadow DOM Web Component Demo</h1>

    <vt-counter-shadow-dom first="10">
        <div class="header">Shadow DOM Counter</div>
    </vt-counter-shadow-dom>
</body>
</html>

The output looks like this:

Image title

Thanks to the <slot> element, the text “Shadow DOM Counter” is displayed within the vt-counter-shadow-dom custom element. (You can even provide named slots, which can be inserted in different parts of the Shadow DOM.) Moreover, the header style class from the including document is applied to the text, while styles declared within the element itself are not visible outside of the element. Child elements are not visible either, which means the <span> element with the id “value” is no longer in danger of having conflicts. In other words, Shadow DOM gives us full encapsulation of the internal structure of our custom element, but still allows the calling document to insert content into the custom element.

View a demo live on Plunker.

Note that Shadow DOM works equally well without HTML Templates; you reap all the same benefits whether you build the DOM from a template, imperatively, or both.

Section 6

Libraries to Simplify your Life

So far we’ve looked at writing Web Components using pure JavaScript and platform APIs. Like most Web Platform technologies, there are libraries that simplify the development process and provide additional features. Here are a few options:

Library Author Description
Polymer Google Polymer is the most popular library for writing Web Components. It provides several additional features such as automatic handling of properties/attributes and two-way property binding, which makes it possible to build entire applications with Web Components alone. It’s heavily used inside of Google for hundreds of projects including YouTube, and even within Chrome itself.
Stencil Ionic Stencil provides a streamlined programming model using TypeScript and JSX that compiles to Vanilla Web Components. Additional features include data-binding, async rendering, Virtual DOM, and server-side rendering.
Skate Skate Team Skate is an extremely lightweight library for writing web components that includes automatic handling of properties/attributes, a fast functional rendering pipeline, and support for different rendering strategies.
X-Tag Microsoft X-Tag simplifies building web components by providing automatic handling of properties/attributes, simplified event handling, and more.
Section 7

Testing

So, how do you test web components anyway? For unit tests, your best bet is Web Component Tester (WCT) from the Polymer team. It’s a handy framework built on top of Selenium, Mocha, and Chai (with a little bit of Sinon). Here’s a simple example:

<!doctype html>
<html>
<head>
  <meta charset="utf-8">
  <script src="../../webcomponentsjs/webcomponents-lite.js"></script>
  <script src="../../web-component-tester/browser.js"></script>
  <link rel="import" href="../awesome-element.html">
</head>
<body>
  <awesome-element id="fixture"></awesome-element>
  <script>
    suite('<awesome-element>', function() {
      test('is awesomest', function() {
        assert.isTrue(document.getElementById('fixture').awesomest);
      });
    });
  </script>
</body>
</html>

For End-to-end (E2E) or Integration tests, all roads lead to an implementation of the WebDriver Spec implemented by tools such as Selenium, Watir, or WebDriver.io. Unfortunately, WebDriver doesn’t yet support Shadow DOM, so you have to use executeJavaScript() to get a reference to the shadow root.

Section 8

Playing with Others

One of the key benefits of Web Components is that they’re part of the Web Platform, so you don’t need a framework to use them. But what if you’re using Angular, React, Vue, or some other framework? Fortunately, generally speaking they work well with most other frameworks, especially if you follow the pattterns presented here and in Google’s excellent HowTo Web Component Examples. Good examples of this are the Vaadin Elements, which work with both Angular and React. And framework interoperability was one of the key reasons Ionic developed Stencil. Most frameworks now have some documentation about how to use Web Components, so consult your framework’s documentation for details. You can also see how your framework's Web Components support stacks up at Custom Elements Everywhere, which runs a battery of compliance tests for each framework and provides a score. 

Section 9

Where Next?

{{ parent.title || parent.header.title}}

{{ parent.tldr }}

{{ parent.urlSource.name }}