Skip to content

Building Components

At the end of this class, you should be able to…

  • Define the core principles of web components and their benefits in front-end development.
  • Create a simple custom web component using the Web Components API.
  • Utilize the shadow DOM to encapsulate styles and DOM structure within components.
  • Implement HTML templates and slots to build reusable and dynamic components.

Additional Notes: Have students read chapter 7 from JavaScript from Beginner to Professional Have students read Using custom elements from MDN


In-Class Demo Jan 2026 term

To run the coding demo, you need to have your Student Workbook open in Visual Studio Code.

  1. Open the terminal window and paste in the following.

    Run from the root of your repository
    pnpm dlx tiged --disable-cache --force DG-InClass/SDEV-1150-A04-Jan-2026/sk/lesson-19 ./src/lesson-19
  2. Walk through the steps in the ReadMe.md of the new lesson.


Students will create and debug a custom web component using the Web Components API.
The lesson focuses on:

  • Understanding customElements.define()
  • Using Shadow DOM for encapsulated styles
  • Working with HTML templates and slots
  • Basic debugging of DOM and Shadow DOM behavior

The project includes the following JS files:

src/
|-- main.js
|-- user-card.js

Open index.html and add a template for a user card:

index.html
...
<body>
<template id="user-card-template">
<div class="card">
<img src="assets/zelda-avatar.png" width="80" height="80" alt="avatar">
<div class="info">
<span class="name">Zelda</span>
<span class="description">Princess of Hyrule</span>
</div>
</div>
</template>
...

NOTE: the template tags will not be rendered by the browser, and we are including an id so we can query for the template in our JS.

In user-card.js, define the class and register it:

user-card.js
class UserCard extends HTMLElement {
constructor() {
super();
const shadow = this.attachShadow({ mode: 'open' });
const template = document.getElementById('user-card-template');
const content = template.content.cloneNode(true);
shadow.appendChild(content);
}
}
customElements.define('user-card', UserCard);
export default UserCard;

Import the UserCard into the main.js file so it is registered with the browser:

main.js
import './user-card.js';

In the index.html, you can now use the custom component. Add a user-card in the body of the document:

index.html
<user-card></user-card>

You should see a user card for Princess Zelda.

Right now, all styling for the user cards is contained in the main.css file, which is not a good idea (why not?). Web components allow for scoping the styling of the component to itself, preventing leakage of styling into the parent document and providing more control over how the component is styled.

Copy the user card styles from main.css into <style> tags inside the <template>. Examine how styles inside the <template> are scoped only to the component.

Modifying the .card background or text color will not affect the global page; change the .card background color in main.css to see if there’s any effect.

We can use attributes to add flexibility and reusability to our components. Add an avatar attribute, which can be used to pass in the desired avatar image source for the component;

<user-card avatar="assets/zelda-avatar.png"></user-card>

Remove the img src in the template markup:

index.html
<div class="card">
<img src="" width="80" height="80" alt="avatar">
<div class="info">
<span class="name">Zelda</span>
<span class="description">Princess of Hyrule</span>
</div>
</div>

We can now update the component class to make use of this passed in avatar attribute:

user-card.js
class UserCard extends HTMLElement {
...
const img = content.querySelector('img');
// if no avatar value is provied, fallback to the placeholder
img.src = this.getAttribute('avatar') || 'https://placehold.co/80x80';
shadow.appendChild(content);
...

Slot values can also be used to pass data to the component. Update the current <user-card> in index.html to accept the following child elements:

index.html
<user-card avatar="assets/zelda-avatar.png">
<span slot="name">Zelda</span>
<span slot="description">Princess of Hyrule</span>
</user-card>

The slot attributes on the spans are used to inform the component about where these elements should be placed within the template. Now, update the template markup to include the necessary slot elements:

index.html
<div class="card">
<img src="" width="80" height="80" alt="avatar">
<div class="info">
<slot name="name" class="name"></slot>
<slot name="description" class="description"></slot>
</div>
</div>

Add a second user card for Link to the index.html to see how our custom component can be reused.

Having the template markup separate from the class definition could be problematic if the component is shared across many pages. Move the template into the user-card.js file to keep all the custom components parts in one place:

user-card.js
const template = document.createElement('template');
template.innerHTML = `
<style>
.card {
background: #ffffff;
color: #222222;
border: 1px solid #e6e6e6;
padding: 12px;
border-radius: 8px;
display: flex;
gap: 12px;
align-items: center;
width: 320px;
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.08);
}
.name {
font-size: 1.2em;
font-weight: bold;
margin: 0;
}
</style>
<div class="card">
<img src="" width="80" height="80" alt="avatar">
<div class="info">
<slot name="name" class="name"></slot>
<slot name="description" class="description"></slot>
</div>
</div>
`;
document.body.appendChild(template);
class UserCard extends HTMLElement {
constructor() {
super();
const shadow = this.attachShadow({ mode: 'open' });
// Use the template defined above, no longer need to query the document
// const template = document.getElementById('user-card-template');
const content = template.content.cloneNode(true);
const img = content.querySelector('img');
img.src = this.getAttribute('avatar') || 'https://placehold.co/80x80';
shadow.appendChild(content);
}
}
customElements.define('user-card', UserCard);
export default UserCard;

Now, the <user-card> web component has its entire definition contained in one file.

We can of course add new <user-card>s using JavaScript as well. Update the main.js to create two additional cards:

import './user-card.js';
// Create an additional user card using HTML and append it to the main element
const dynamicUserCard = `
<user-card avatar="https://placehold.co/80x80/7700ff/ffffff">
<span slot="name">Mipha</span>
<span slot="description">Zora Champion</span>
</user-card>`;
document.querySelector('main').insertAdjacentHTML('beforeend', dynamicUserCard);
// Create another user card using JavaScript DOM API only and append it to the main element
const anotherUserCard = document.createElement('user-card');
anotherUserCard.setAttribute('avatar', 'https://placehold.co/80x80/770000/ffffff');
const nameSpan = document.createElement('span');
nameSpan.setAttribute('slot', 'name');
nameSpan.textContent = 'Yunobo';
const descSpan = document.createElement('span');
descSpan.setAttribute('slot', 'description');
descSpan.textContent = 'President of YunoboCo';
anotherUserCard.appendChild(nameSpan);
anotherUserCard.appendChild(descSpan);
document.querySelector('main').appendChild(anotherUserCard);
// Why doesn't the custom avatar show up for this element (answered in a future lesson)?

Add a “Details” slot within the shadow DOM that shows a random “status” message (e.g., “Ready for adventure!”).

IssueCauseSolution
Component not appearingcustomElements.define missing or misspelledDefine before first use
Styles not appliedAdded styles outside shadow rootMove <style> into the shadow DOM
Slots emptySlot name mismatchMatch slot attributes in both template and element
Template not foundgetElementById or querySelector returns nullEnsure <template id="user-card-template"> exists before script runs

Once you’re done making your own custom updates to the project, stage your files, commit your work, and push to the remote repository.

  1. Open a terminal in VS Code
  2. Stage all updated and created files:
Terminal window
git add .
  1. Commit the changes:
Terminal window
git commit -m 'Lesson 19 Example'
  1. Push your changes to the remote workbook repository:
Terminal window
git push origin main