The idea of creating your own HTML tags feels like a superpower. You can build a custom element that works in any project, regardless of the framework. No more copying and pasting the same card or modal code across different files. Web components let you package structure, style, and behavior into a single, reusable piece. And the best part? You already know the languages you need: HTML, CSS, and JavaScript. In 2026, browser support is rock solid. There is no better time to learn how to build a web component.
This guide teaches you how to build a web component using plain JavaScript. You will create a custom element called `
What Makes a Web Component a Web Component
Before you write any code, it helps to understand the three building blocks. A web component combines custom elements, the shadow DOM, and HTML templates. You do not always need all three, but together they give you full control.
Custom elements let you define your own HTML tag. You write a class that extends HTMLElement and register it with the browser. The shadow DOM provides style and markup isolation. Styles inside a shadow tree do not leak out, and external styles do not leak in. HTML templates let you define chunks of markup that you can reuse. For this tutorial, you will focus on custom elements and the shadow DOM.
Setting Up Your Project
Create a new folder and add two files: index.html and user-card.js. You will write everything in plain JavaScript. No build tools. No npm install. This keeps the focus on the core concepts.
Your index.html should look like this:
<!DOCTYPE html>
<html lang="en">
<head>
</head>
<body>
<user-card
name="Alex Rivera"
role="Frontend Developer"
avatar="https://i.pravatar.cc/80?img=3">
</user-card>
</body>
</html>
Notice the custom tag <user-card>. The browser does not know what to do with it yet. That is where your JavaScript file comes in.
Writing the Component Class
Open user-card.js and start with the class definition.
class UserCard extends HTMLElement {
constructor() {
super();
this.attachShadow({ mode: 'open' });
}
connectedCallback() {
this.render();
}
render() {
const name = this.getAttribute('name') || 'Guest';
const role = this.getAttribute('role') || 'Unknown Role';
const avatar = this.getAttribute('avatar') || 'https://i.pravatar.cc/80?img=1';
this.shadowRoot.innerHTML = `
<div class="card">
<img src="${avatar}" alt="Avatar for ${name}" />
<div class="info">
<span class="name">${name}</span>
<span class="role">${role}</span>
</div>
</div>
`;
}
}
customElements.define('user-card', UserCard);
Let us break down what is happening here.
The constructor calls super() and then attachShadow({ mode: 'open' }). This creates a shadow tree attached to your custom element. The mode: 'open' means other JavaScript can access the shadow root if needed. You could use 'closed' for stricter encapsulation, but open is friendlier for learning.
The connectedCallback() method fires every time the element is inserted into the DOM. That is your trigger to render the content. Inside render(), you read the attributes using getAttribute() and provide fallback values in case they are missing. Then you set the innerHTML of the shadow root.
The styles inside the <style> tag are scoped to the shadow tree. They will not affect anything outside the component. This is the encapsulation that makes web components so reliable.
Finally, customElements.define('user-card', UserCard) registers your class with the tag name. The tag name must contain a hyphen. This is a requirement of the custom elements spec.
Handling Dynamic Updates with attributeChangedCallback
Attributes can change after the component is already on the page. You want the card to update automatically. Add two more pieces to your class.
First, define a static getter that tells the browser which attributes to watch.
static get observedAttributes() {
return ['name', 'role', 'avatar'];
}
Second, implement attributeChangedCallback. This method runs whenever one of the observed attributes changes.
attributeChangedCallback(name, oldValue, newValue) {
if (oldValue !== newValue) {
this.render();
}
}
Now if someone changes the name attribute of your <user-card> element, the card re-renders with the new data. This keeps your component responsive without any extra effort.
Common Mistakes and How to Avoid Them
Even experienced developers hit a few snags when building their first component. Here is a table of frequent issues and their fixes.
| Mistake | What Happens | How to Fix It |
|---|---|---|
| Forgetting the hyphen in the tag name | The registration fails silently | Use a name like user-card or my-button |
Not calling super() in the constructor |
The element does not initialize properly | Always call super() as the first line |
| Setting styles outside the shadow DOM | Styles leak and conflict | Put all component styles inside the shadow root |
Using innerHTML without sanitizing user input |
Security risk | Sanitize or use textContent for plain strings |
Omitting observedAttributes |
attributeChangedCallback never fires |
Add the static getter with the correct attribute names |
When to Use Templates
For a simple card, setting innerHTML directly works fine. For more complex components with repeated structures, you might use an <template> element. Templates let you clone markup without parsing HTML strings. They are especially useful for lists or data grids.
Here is an example of how you would load a template inside the shadow root.
const template = document.createElement('template');
template.innerHTML = `
<div class="card">
<img />
<div class="info">
<span class="name"></span>
<span class="role"></span>
</div>
</div>
`;
this.shadowRoot.appendChild(template.content.cloneNode(true));
Then you would query the elements inside the shadow root and set their values. This approach is slightly more verbose but avoids re-parsing HTML on every update.
Testing Your Component
Open index.html in a browser. You should see a card with a profile picture, the name “Alex Rivera”, and the role “Frontend Developer”. Try changing the attribute values in the HTML file and refreshing. The card updates.
You can also inspect the element using your browser’s developer tools. Look for the #shadow-root marker. This shows you the boundary between the light DOM and the shadow DOM.
Expert advice: Use the developer tools to explore how other web components work. Open a page that uses a custom element and inspect its shadow root. You will see the same patterns you just learned. This is the best way to build confidence.
Going Further with Slots
Slots let users of your component insert content into specific areas. You can define a slot inside the shadow DOM, and any content placed inside the custom element in the light DOM will appear there.
Add a slot to your card for an optional badge.
<div class="card">
<img src="${avatar}" alt="Avatar for ${name}" />
<div class="info">
<span class="name">${name}</span>
<span class="role">${role}</span>
<slot name="badge"></slot>
</div>
</div>
Now in your HTML, you can use it like this.
<user-card
name="Alex Rivera"
role="Frontend Developer"
avatar="https://i.pravatar.cc/80?img=3">
<span slot="badge" style="background:gold; padding:2px 8px; border-radius:4px;">Pro</span>
</user-card>
The text “Pro” appears inside the card, styled by the light DOM but positioned by the slot. This mix of control and flexibility is what makes web components powerful.
Your Next Steps with Web Components
You have built a working web component from scratch. You understand custom elements, the shadow DOM, attribute callbacks, and slots. That is the foundation for creating any reusable UI piece.
Try building a simple rating stars component or a collapsible panel next. Each one will reinforce the same patterns. As you get comfortable, you can look into libraries like Lit that provide helpers for common tasks. But the raw approach you learned here will always serve you well.
For a broader view of how these pieces fit into modern frontend development, check out the Unlocking the Power of Web Components for Modern Frontend Development article. If you want to make sure your components work for everyone, read How to Build Accessible Web Components That Everyone Can Use. And for a look at what is coming next, see Why Web Components Are Finally Ready for Production in 2026.
Writing Components That Last
Web components are not a trend. They are a standard built into every modern browser. The code you write today will still work years from now without updates or dependency changes. That is rare in web development.
Take the component you just built and drop it into a personal project. Add a new feature like a click handler or a tooltip. Break it on purpose and fix it. That hands-on experience is worth more than any documentation.
You now know how to build a web component. Go make something with it.