I’ve to thank Jeremy Keith and his splendidly insightful article from late final yr that launched me to the idea of HTML Net Parts. This was the “a-ha!” second for me:
Whenever you wrap some current markup in a customized aspect after which apply some new behaviour with JavaScript, technically you’re not doing something you couldn’t have finished earlier than with some DOM traversal and occasion dealing with. However it’s much less fragile to do it with an online part. It’s moveable. It obeys the one accountability precept. It solely does one factor however it does it effectively.
Till then, I’d been underneath the false assumption that all internet elements rely solely on the presence of JavaScript at the side of the somewhat scary-sounding Shadow DOM. Whereas it’s certainly potential to writer internet elements this fashion, there may be yet one more manner. A greater manner, maybe? Particularly in case you, like me, advocate for progressive enhancement. HTML Net Parts are, in any case, simply HTML.
Whereas it’s outdoors the precise scope of what we’re discussing right here, Any Bell has a current write-up that gives his (wonderful) tackle what progressive enhancement means.
Let’s take a look at three particular examples that exhibit what I believe are the important thing options of HTML Net Parts — CSS type encapsulation and alternatives for progressive enhancement — with out being pressured to rely upon JavaScript to work out of the field. We’ll most positively use JavaScript, however the elements must work with out it.
The examples can all be present in my Net UI Boilerplate part library (constructed utilizing Storybook), together with the related supply code in GitHub.
<webui-disclosure>
Instance 1: 
I actually like how Chris Ferdinandi teaches constructing an online part from scratch, utilizing a disclosure (present/conceal) sample for example. This primary instance extends his demo.
Let’s begin with the first-class citizen, HTML. Net elements permit us to determine customized parts with our personal naming, which is the case on this instance with a <webui-disclosure>
tag we’re utilizing to carry a <button>
designed to point out/conceal a block of textual content and a <div>
that holds the <p>
of textual content we need to present and conceal.
<webui-disclosure
data-bind-escape-key
data-bind-click-outside
>
<button
kind="button"
class="button button--text"
data-trigger
hidden
>
Present / Conceal
</button>
<div data-content>
<p>Content material to be proven/hidden.</p>
</div>
</webui-disclosure>
If JavaScript is disabled or doesn’t execute (for any variety of potential causes), the button is hidden by default — due to the hidden
attribute on it— and the content material inside the div is solely displayed by default.
Good. That’s a very easy instance of progressive enhancement at work. A customer can view the content material with or with out the <button>
.
I discussed that this instance extends Chris Ferdinandi’s preliminary demo. The important thing distinction is which you can shut the aspect both by clicking the keyboard’s ESC
key or clicking anyplace outdoors the aspect. That’s what the 2 [data-attribute]
s on the <webui-disclosure
tag are for.
We begin by defining the customized aspect in order that the browser is aware of what to do with our made-up tag identify:
customElements.outline('webui-disclosure', WebUIDisclosure);
Customized parts have to be named with a dashed-ident, reminiscent of <my-pizza>
or no matter, however as Jim Neilsen notes, by means of Scott Jehl, that doesn’t precisely imply that the sprint has to go between two phrases.
I sometimes want utilizing TypeScript for writing JavaScript to assist eradicate silly errors and implement some extent of “defensive” programming. However for the sake of simplicity, the construction of the net part’s ES Module appears to be like like this in plain JavaScript:
default class WebUIDisclosure extends HTMLElement {
constructor()
setupA11y() {
// Add ARIA props/state to button.
}
// Deal with constructor() occasion listeners.
handleEvent(e) {
// 1. Toggle visibility of content material.
// 2. Toggle ARIA expanded state on button.
}
// Deal with occasion listeners which aren't a part of this Net Element.
connectedCallback() {
doc.addEventListener('keyup', (e) => {
// Deal with ESC key.
});
doc.addEventListener('click on', (e) => {
// Deal with clicking outdoors.
});
}
disconnectedCallback() {
// Take away occasion listeners.
}
}
Are you questioning about these occasion listeners? The primary one is outlined within the constructor()
operate, whereas the remaining are within the connectedCallback()
operate. Hawk Ticehurst explains the rationale rather more eloquently than I can.
This JavaScript isn’t required for the net part to “work” however it does sprinkle in some good performance, to not point out accessibility issues, to assist with the progressive enhancement that permits the <button>
to point out and conceal the content material. For instance, JavaScript injects the suitable aria-expanded
and aria-controls
attributes enabling those that depend on display readers to grasp the button’s function.
That’s the progressive enhancement piece to this instance.
For simplicity, I’ve not written any further CSS for this part. The styling you see is solely inherited from current world scope or part kinds (e.g., typography and button).
Nonetheless, the subsequent instance does have some further scoped CSS.
<webui-tabs>
Instance 2: That first instance lays out the progressive enhancement advantages of HTML Net Parts. One other profit we get is that CSS kinds are encapsulated, which is a elaborate manner of claiming the CSS doesn’t leak out of the part. The kinds are scoped purely to the net part and people kinds won’t battle with different kinds utilized to the present web page.
Let’s flip to a second instance, this time demonstrating the type encapsulating powers of internet elements and how they assist progressive enhancement in consumer experiences. We’ll be utilizing a tabbed part for organizing content material in “panels” which might be revealed when a panel’s corresponding tab is clicked — the identical form of factor you’ll discover in lots of part libraries.

Beginning with the HTML construction:
<webui-tabs>
<div data-tablist>
<a href="#tab1" data-tab>Tab 1</a>
<a href="#tab2" data-tab>Tab 2</a>
<a href="#tab3" data-tab>Tab 3</a>
</div>
<div id="tab1" data-tabpanel>
<p>1 - Lorem ipsum dolor sit amet consectetur.</p>
</div>
<div id="tab2" data-tabpanel>
<p>2 - Lorem ipsum dolor sit amet consectetur.</p>
</div>
<div id="tab3" data-tabpanel>
<p>3 - Lorem ipsum dolor sit amet consectetur.</p>
</div>
</webui-tabs>
You get the thought: three hyperlinks styled as tabs that, when clicked, open a tab panel holding content material. Notice that every [data-tab]
within the tab checklist targets an anchor hyperlink matching a tab panel ID, e.g., #tab1
, #tab2
, and many others.
We’ll take a look at the type encapsulation stuff first since we didn’t go there within the final instance. Let’s say the CSS is organized like this:
webui-tabs {
[data-tablist] {
/* Default kinds with out JavaScript */
}
[data-tab] {
/* Default kinds with out JavaScript */
}
[role="tablist"] {
/* Fashion function added by JavaScript */
}
[role="tab"] {
/* Fashion function added by JavaScript */
}
[role="tabpanel"] {
/* Fashion function added by JavaScript */
}
}
See what’s taking place right here? We have now two type guidelines — [data-tablist]
and [data-tab]
— that comprise the net part’s default kinds. In different phrases, these kinds are there no matter whether or not JavaScript masses or not. In the meantime, the opposite three type guidelines are selectors which might be injected into the part so long as JavaScript is enabled and supported. This fashion, the final three type guidelines are solely utilized if JavaScript plops the **function**
attribute on these parts within the HTML. Proper there, we’re already supplying a contact of progressive enhancement by setting kinds solely when JavasScript is required.
All these kinds are absolutely encapsulated, or scoped, to the <webui-tabs>
internet part. There is no such thing as a “leakage” so to talk that will bleed into the kinds of different internet elements, and even to the rest on the web page inside the world scope. We are able to even select to forego classnames, advanced selectors, and methodologies like BEM in favour of straightforward descendent selectors for the part’s youngsters, permitting us to put in writing kinds extra declaratively on semantic parts.
Rapidly: “Gentle” DOM versus Shadow DOM
For many internet tasks, I usually want to bundle CSS (together with the internet part Sass partials) right into a single CSS file in order that the part’s default kinds can be found within the world scope, even when the JavaScript doesn’t execute.
Nonetheless, it’s potential to import a stylesheet through JavaScript that’s solely consumed by this internet part if JavaScript is offered:
import kinds from './kinds.css';
class WebUITabs extends HTMLElement {
constructor() {
tremendous();
this.adoptedStyleSheets = [styles];
}
}
customElements.outline('webui-tabs', WebUITabs);
Alternatively, we may inject a <type>
tag containing the part’s kinds as a substitute:
class WebUITabs extends HTMLElement {
connectedCallback() {
this.attachShadow({ mode: 'open' }); // Required for JavaScript entry
this.shadowRoot.innerHTML = `
<type> <!-- kinds go right here --> </type>
// and many others.
`;
}
}
customElements.outline('webui-tabs', WebUITabs);
Whichever technique you select, these kinds are scoped on to the net part, stopping part kinds from leaking out, however permitting world kinds to be inherited.
Now contemplate this easy instance. All the things we write in between the part’s opening and shutting tags is taken into account a part of the “Gentle” DOM.
<my-web-component>
<!-- That is Gentle DOM -->
<div>
<p>Some content material... kinds are inherited from the worldwide scope</p>
</div>
----------- Shadow DOM Boundary -------------
| <!-- Something injected by JavaScript --> |
---------------------------------------------
</my-web-component>
Dave Rupert has a superb write-up that makes it very easy to see how exterior kinds are capable of “pierce” the Shadow DOM and choose a component within the Gentle DOM. Discover how the <button>
aspect that’s written in between the customized aspect’s tags receives the button
selector’s kinds within the world CSS, whereas the <button>
injected through JavaScript is left untouched.
If we need to type the Shadow DOM <button>
we’d have to try this with inside kinds just like the examples above for importing a stylesheet or injecting an inline <type>
block.
That doesn’t imply that all CSS type properties are blocked by the Shadow DOM. In truth, Dave outlines 37 properties that internet elements inherit
, principally alongside the traces of textual content, checklist, and desk formatting.
Progressively improve the tabbed part with JavaScript
Regardless that this second instance is extra about type encapsulation, it’s nonetheless an excellent alternative to see the progressive enhancement we get virtually at no cost from internet elements. Let’s step into the JavaScript now so we will see how we will assist progressive enhancement. The complete code is sort of prolonged, so I’ve abbreviated issues a bit to assist make the factors a little bit clearer.
default class WebUITabs extends HTMLElement {
constructor() {
tremendous();
this.tablist = this.querySelector('[data-tablist]');
this.tabpanels = this.querySelectorAll('[data-tabpanel]');
this.tabTriggers = this.querySelectorAll('[data-tab]');
if (
!this.tablist ||
this.tabpanels.size === 0 ||
this.tabTriggers.size === 0
) return;
this.createTabs();
this.tabTriggers.forEach((tabTrigger, index) => {
tabTrigger.addEventListener('click on', (e) => {
this.bindClickEvent(e);
});
tabTrigger.addEventListener('keydown', (e) => {
this.bindKeyboardEvent(e, index);
});
});
}
createTabs() {
// 1. Conceal all tabpanels initially.
// 2. Add ARIA props/state to tabs & tabpanels.
}
bindClickEvent(e) {
e.preventDefault();
// Present clicked tab and replace ARIA props/state.
}
bindKeyboardEvent(e, index) {
e.preventDefault();
// Deal with keyboard ARROW/HOME/END keys.
}
}
customElements.outline('webui-tabs', WebUITabs);
The JavaScript injects ARIA roles, states, and props to the tabs and content material blocks for display reader customers, in addition to further keyboard bindings so we will navigate between tabs with the keyboard; for instance, the TAB
key’s reserved for accessing the part’s lively tab and any focusable content material contained in the lively tabpanel
, and the tabs may be traversed with the ARROW
keys. So, if JavaScript fails to load, the default expertise continues to be an accessible one the place the tabs nonetheless anchor hyperlink to their respective panels, and people panels naturally stack vertically, one on prime of the opposite.
And if JavaScript is enabled and supported? We get an enhanced expertise, full with up to date accessibility issues.
<webui-ajax-loader>
Instance 3: 
This last instance differs from the earlier two in that it’s completely generated by JavaScript, and makes use of the Shadow DOM. It is because it is just used to point a “loading” state for Ajax requests, and is subsequently solely wanted when JavaScript is enabled.
The HTML markup is simply the opening and shutting part tags:
<webui-ajax-loader></webui-ajax-loader>
The simplified JavaScript construction:
default class WebUIAjaxLoader extends HTMLElement {
constructor() {
tremendous();
const shadow = this.attachShadow({ mode: 'open' });
shadow.innerHTML = `
<svg function="img" half="svg">
<title>loading</title>
<circle cx="50" cy="50" r="47" />
</svg>
`;
}
}
customElements.outline('webui-ajax-loader',WebUIAjaxLoader);
Discover proper out of the gate that all the things in between the <webui-ajax-loader>
tags is injected with JavaScript, that means it’s all within the Shadow DOM, encapsulated from different scripts and kinds circuitously bundled with the part.
But additionally discover the half
attribute that’s set on the <svg>
aspect. Right here’s the place we’ll zoom in:
<svg function="img" half="svg">
<!-- and many others. -->
</svg>
That’s yet one more manner we have now to type the customized aspect: named elements. Now we will type that SVG from outdoors of the template literal we used to determine the aspect. There’s a ::half
pseudo-selector to make that occur:
webui-ajax-loader::half(svg) {
// Shadow DOM kinds for the SVG...
}
And right here’s one thing cool: that selector can entry CSS customized properties, whether or not they’re scoped globally or domestically to the aspect.
webui-ajax-loader {
--fill: orangered;
}
webui-ajax-loader::half(svg) {
fill: var(--fill);
}
So far as progressive enhancement goes, JavaScript provides all the HTML. Which means the loader is just rendered if JavaScript is enabled and supported. And when it’s, the SVG is added, full with an accessible title and all.
Wrapping up
That’s it for the examples! What I hope is that you simply now have the identical form of epiphany that I had when studying Jeremy Keith’s put up: HTML Net Parts are an HTML-first characteristic.
In fact, JavaScript does play an enormous function, however solely as large as wanted. Want extra encapsulation? Need to sprinkle in some UX goodness when a customer’s browser helps it? That’s what JavaScript is for and that’s what makes HTML Net Parts such a terrific addition to the net platform — they depend on vanilla internet languages to do what they had been designed to do all alongside, and with out leaning too closely on one or the opposite.