Lesson 15: Components
Web components let you build a custom HTML tag once and reuse it as many times as you want. You don’t need any complex tools for this because web browsers have this feature built right in.
A web component is made of three main parts. These are templates, custom tags, and the shadow DOM. Together, these features let you build isolated elements that work just like standard HTML tags.
We’ll build a <star-rating> element to let users rate things from one to five
stars. Custom elements must use lowercase names containing a dash so the browser
doesn’t confuse them with native tags.
The <template> element
The <template> tag holds HTML code that the browser doesn’t show when the page
loads. Instead, you’ll use JavaScript to copy this template and insert it into
the page when you need it.
<template id="star-rating-template">
<form>
<fieldset>
<legend>Rate your experience:</legend>
<rating>
<input
type="radio"
name="rating"
value="1"
aria-label="1 star"
required
/>
<input type="radio" name="rating" value="2" aria-label="2 stars" />
<input type="radio" name="rating" value="3" aria-label="3 stars" />
<input type="radio" name="rating" value="4" aria-label="4 stars" />
<input type="radio" name="rating" value="5" aria-label="5 stars" />
</rating>
</fieldset>
<button type="reset">Reset</button>
<button type="submit">Submit</button>
</form>
</template>Since the template contents are hidden, the form doesn’t show up right away. You can grab this hidden content using JavaScript and place it onto your page. This is useful when you want to repeat the same block of HTML in multiple places without rewriting it.
The <slot> element
We can use the <slot> tag as a placeholder inside our template. This creates
an empty space where you can slide in custom text when you use the component.
<template id="star-rating-template">
<form>
<fieldset>
<slot name="star-rating-legend">
<legend>Rate your experience:</legend>
</slot>
</fieldset>
</form>
</template>The name attribute defines the slot. When you use the custom tag, any element
with a matching slot attribute will render inside that placeholder. If you
don’t provide any custom text, the default text inside the slot tag will show
instead.
<star-rating>
<legend slot="star-rating-legend">Blendan Smooth</legend>
</star-rating>
<star-rating>
<legend slot="star-rating-legend">Hoover Sukhdeep</legend>
</star-rating>
<star-rating>
<legend slot="star-rating-legend">Toasty McToastface</legend>
<p>Is this text visible?</p>
</star-rating>Undefined elements
If you use a tag that the browser doesn’t recognize, the page won’t break. The
browser just treats it as a plain inline text box, similar to a <span>, with
no default styles.
Until we define our custom tag using JavaScript, the browser will just show its children as plain text. We need to register the tag with the browser to make it work.
Custom elements
To register a custom tag, we use a tiny bit of JavaScript to tell the browser what our tag should do.
customElements.define(
"star-rating",
class extends HTMLElement {
constructor() {
super();
const starRating = document.getElementById(
"star-rating-template",
).content;
const shadowRoot = this.attachShadow({
mode: "open",
});
shadowRoot.appendChild(starRating.cloneNode(true));
}
},
);Now, whenever the browser finds our custom tag, it will copy the template content inside it. Because this content is isolated, global CSS styles on your page won’t apply to it. You’ll need to write specific styles inside the template to style it.
Shadow DOM
The Shadow DOM is a way to keep your component styles separate from the rest of the page. Global styles won’t affect the inside of your component, and your component styles won’t leak out to affect other elements.
We can add a <style> block inside our template to style the component
directly. Because the styles are isolated, we can use simple selectors like
input without worrying about styling inputs elsewhere on the page.
<template id="star-rating-template">
<style>
rating {
display: inline-flex;
}
input {
appearance: none;
margin: 0;
box-shadow: none;
}
input::after {
content: "\2605"; /* solid star */
font-size: 32px;
}
rating:hover input:invalid::after,
rating:focus-within input:invalid::after {
color: #888;
}
input:invalid::after,
rating:hover input:hover ~ input:invalid::after,
input:focus ~ input:invalid::after {
color: #ddd;
}
input:valid {
color: orange;
}
input:checked ~ input:not(:checked)::after {
color: #ccc;
content: "\2606"; /* hollow star */
}
</style>
<form>
<fieldset>
<slot name="star-rating-legend">
<legend>Rate your experience:</legend>
</slot>
<rating>
<input
type="radio"
name="rating"
value="1"
aria-label="1 star"
required
/>
<input type="radio" name="rating" value="2" aria-label="2 stars" />
<input type="radio" name="rating" value="3" aria-label="3 stars" />
<input type="radio" name="rating" value="4" aria-label="4 stars" />
<input type="radio" name="rating" value="5" aria-label="5 stars" />
</rating>
</fieldset>
<button type="reset">Reset</button>
<button type="submit">Submit</button>
</form>
</template>Slotted content is not fully isolated. The legend element injected into our slot is styled by the global page styles, not by the component styles.
Styling across boundaries
You can style across the isolation boundary, but you have to do it intentionally.
Inside the component stylesheet, the :host selector targets the custom element
itself.
The ::slotted() selector lets you style elements that were slid into slots
from the outside.
If you want global styles to affect elements inside the component, you can add a
part attribute to them. You can then target them in your main CSS file using
::part().
<template id="star-rating-template">
<form part="formPart">
<fieldset part="fieldsetPart"></fieldset>
</form>
</template>star-rating::part(formPart) {
background-color: #f5f5f5;
}Part attributes work like classes. Multiple elements can share the same part name, and a single element can have multiple parts.
Adapted from Learn HTML © Google and contributors, licensed under CC BY 4.0 (prose) and Apache 2.0 (code samples).