Building Components
Learning Outcome Guide
Section titled “Learning Outcome Guide”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
Coding Demo
Section titled “Coding Demo”In-Class Demo Jan 2026 term
To run the coding demo, you need to have your Student Workbook open in Visual Studio Code.
-
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 -
Walk through the steps in the
ReadMe.mdof the new lesson.
Objectives
Section titled “Objectives”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
Starter Files
Section titled “Starter Files”The project includes the following JS files:
src/|-- main.js|-- user-card.jsInstructor Demo
Section titled “Instructor Demo”Create the Template
Section titled “Create the Template”Open index.html and add a template for a user card:
...<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
idso we can query for the template in our JS.
Create the Component Logic
Section titled “Create the Component Logic”In user-card.js, define the class and register it:
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:
import './user-card.js';Render A user-card
Section titled “Render A user-card”In the index.html, you can now use the custom component. Add a user-card in the body of the document:
<user-card></user-card>You should see a user card for Princess Zelda.
Add Shadow DOM Styles
Section titled “Add Shadow DOM Styles”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.
Add Attributes and Slots
Section titled “Add Attributes and Slots”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:
<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:
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:
<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:
<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.
Self-Contained Templates
Section titled “Self-Contained Templates”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:
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.
Add <user-card>s Programatically
Section titled “Add <user-card>s Programatically”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 elementconst 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 elementconst 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)?Student Exercise
Section titled “Student Exercise”Extend the Component
Section titled “Extend the Component”Add a “Details” slot within the shadow DOM that shows a random “status” message (e.g., “Ready for adventure!”).
Common Errors & Fixes
Section titled “Common Errors & Fixes”| Issue | Cause | Solution |
|---|---|---|
| Component not appearing | customElements.define missing or misspelled | Define before first use |
| Styles not applied | Added styles outside shadow root | Move <style> into the shadow DOM |
| Slots empty | Slot name mismatch | Match slot attributes in both template and element |
| Template not found | getElementById or querySelector returns null | Ensure <template id="user-card-template"> exists before script runs |
Push to Your GitHub Workbook Repo
Section titled “Push to Your GitHub Workbook Repo”Once you’re done making your own custom updates to the project, stage your files, commit your work, and push to the remote repository.
- Open a terminal in VS Code
- Stage all updated and created files:
git add .- Commit the changes:
git commit -m 'Lesson 19 Example'- Push your changes to the remote workbook repository:
git push origin main