I use native Web Components when a site needs repeatable interface elements without a framework or build process. Learning how to create reusable web components with vanilla JavaScript lets you package markup, styling, behavior, and a public API inside one custom HTML element.
The browser already provides the required tools. Custom Elements define new tags, Shadow DOM can isolate internal markup and styles, and templates with slots create reusable structures. MDN identifies these technologies as the main building blocks of Web Components.
What Makes a Vanilla JavaScript Component Reusable?
A reusable component should work across several pages without copying its internal code. It should accept controlled inputs, expose useful outputs, and reverse any side effects it creates.
The HTML standard lets developers define functional custom elements. Their names must contain a hyphen, which keeps author-defined tags separate from native HTML elements.
When I plan how to create reusable web components with vanilla JavaScript, I separate the work into four parts:
- A custom element class for behavior
- A template for repeatable markup
- Shadow DOM for optional encapsulation
- Attributes, properties, slots, and events for communication
Shadow DOM helps when unknown page CSS could break a component. It also keeps internal styles scoped to the shadow tree. However, it should not replace semantic HTML or a clear public API.
How To Create Reusable Web Components With Vanilla JavaScript Step by Step

This example creates a <user-card> element with a named slot, a role attribute, and a button that changes the role. It stores its event handler, so cleanup uses the same function reference.
Create the Template and Encapsulated Styles
const template = document.createElement(‘template’);
template.innerHTML = `
<style>
:host {
display: block;
max-width: 280px;
font-family: system-ui, sans-serif;
}
.card {
padding: 1rem;
border: 1px solid #d7d7d7;
border-radius: 0.5rem;
background: #fff;
}
button {
padding: 0.5rem 0.75rem;
cursor: pointer;
}
</style>
<article class=”card”>
<h2><slot name=”username”>Default User</slot></h2>
<p>Role: <span id=”role-text”>Member</span></p>
<button id=”toggle-btn” type=”button”>Change role</button>
</article>
`;
A <template> stores markup without rendering it immediately. Cloning its content gives each instance its own DOM structure. Named slots let page authors replace selected content while preserving the component layout.
Define the Custom Element Class

class UserCard extends HTMLElement {
static observedAttributes = [‘role’];
constructor() {
super();
this.attachShadow({ mode: ‘open’ });
this.shadowRoot.append(template.content.cloneNode(true));
this.roleText = this.shadowRoot.getElementById(‘role-text’);
this.toggleButton = this.shadowRoot.getElementById(‘toggle-btn’);
this.handleToggle = this.handleToggle.bind(this);
}
connectedCallback() {
this.renderRole();
this.toggleButton.addEventListener(‘click’, this.handleToggle);
}
disconnectedCallback() {
this.toggleButton.removeEventListener(‘click’, this.handleToggle);
}
attributeChangedCallback(name, oldValue, newValue) {
if (name === ‘role’ && oldValue !== newValue) {
this.renderRole();
}
}
renderRole() {
this.roleText.textContent = this.getAttribute(‘role’) || ‘Member’;
}
handleToggle() {
const currentRole = this.getAttribute(‘role’) || ‘Member’;
const nextRole = currentRole === ‘Member’ ? ‘Admin’ : ‘Member’;
this.setAttribute(‘role’, nextRole);
this.dispatchEvent(
new CustomEvent(‘role-change’, {
detail: { role: nextRole },
bubbles: true,
composed: true
})
);
}
}
The constructor creates the shadow tree and stores element references. connectedCallback() runs when the element enters the document. disconnectedCallback() handles cleanup. attributeChangedCallback() reacts to values listed in observedAttributes.
Using separate arrow functions in addEventListener() and removeEventListener() fails because they are different function objects. Binding handleToggle once gives both methods the same reference. MDN confirms that listener removal requires the same event type and listener reference.
Register and Use the Component
if (!customElements.get(‘user-card’)) {
customElements.define(‘user-card’, UserCard);
}
<user-card role=”Admin”>
<span slot=”username”>Jane Doe</span>
</user-card>
<script type=”module”>
document.addEventListener(‘role-change’, (event) => {
console.log(`New role: ${event.detail.role}`);
});
</script>
The guard avoids duplicate registration during repeated module loading. Once registered, the component can appear in HTML like a native element.
Use the Three-Contract Rule
My most useful rule for how to create reusable web components with vanilla JavaScript is simple: define the input contract, output contract, and cleanup contract before adding features.
Define Inputs
Use attributes for short, serializable values such as role, size, or disabled. Use properties for objects, arrays, functions, and frequently changing state.
Avoid storing large JSON objects in attributes unless serialization is necessary. A property such as card.user = userObject is easier to read and maintain.
Define Outputs
A reusable element should not reach into unrelated page code. Dispatch a custom event instead. The parent page can listen and choose the next action.
Set bubbles: true when ancestor elements should receive the event. Set composed: true when it must cross a Shadow DOM boundary.
Clean Up Side Effects
Remove document-level listeners, stop timers, abort pending requests, and disconnect observers when the element is removed. This matters in single-page applications where the same component may mount repeatedly.
Reliable cleanup is a central part of how to create reusable web components with vanilla JavaScript, not an optional performance tweak.
Improve Accessibility, Styling, and Security

Good accessibility belongs inside how to create reusable web components with vanilla JavaScript from the first draft. Use native semantic controls whenever possible. A real <button> already supports keyboard interaction that a clickable <div> lacks. Web.dev recommends preserving native semantics and avoiding behavior that surprises users.
Expose deliberate styling hooks instead of the entire internal structure. CSS custom properties work well for colors and spacing. A part attribute with ::part() can expose selected shadow elements for outside styling.
Avoid placing untrusted strings into innerHTML. Use textContent for plain text and DOM methods for structured content. For components containing forms, pair this architecture with how to prevent spam submissions on website contact forms so reuse does not multiply weak validation or abuse risks.
Common Web Component Mistakes
These mistakes cause most reuse problems:
- Anonymous event handlers that cannot be removed
- Full shadow-root rerenders for minor text changes
- Large objects forced into HTML attributes
- Inflexible styles with no supported customization hooks
- Missing labels, focus states, or keyboard behavior
When deciding how to create reusable web components with vanilla JavaScript, start with the smallest stable API. Adding a new attribute later is easier than removing one after several pages depend on it.
Web Components suit design-system controls, CMS blocks, static sites, and widgets shared across different technology stacks. They use browser standards, so one custom element can remain independent of a specific framework.
That narrow, standards-based approach is how to create reusable web components with vanilla JavaScript without rebuilding a full application framework.
Frequently Asked Questions
1. Can I create Web Components without a framework?
Yes. Custom Elements, Shadow DOM, templates, slots, modules, and browser events are native platform features.
2. Do reusable Web Components require Shadow DOM?
No. Shadow DOM is optional, but it helps isolate internal markup and styles from unknown page CSS.
3. How do I pass data into a vanilla JavaScript component?
Use attributes for simple values and properties for objects, arrays, callbacks, or frequently changing data.
4. How do I create reusable Web Components with vanilla JavaScript safely?
Use semantic HTML, avoid untrusted HTML injection, expose a small API, dispatch events, and clean up every side effect.
Ship the Component, Skip the Framework Drama
I prefer how to create reusable web components with vanilla JavaScript when the goal is portability, not a miniature framework hidden inside one file. A strong component has a narrow purpose, predictable data flow, accessible markup, and dependable cleanup.
Build one small element first. Add several instances to one page, change their attributes, remove them, and add them again. Confirm that each event fires only once. That quick test catches reuse bugs before they spread across a site.






























