Building Dynamic Web Components Using Vanilla JavaScript

Building Dynamic Web Components Using Vanilla JavaScript

Why Vanilla JavaScript for Web Components?

Alright, let’s kick this off with a little confession: I get it — the word “vanilla JavaScript” sometimes feels like a throwback to simpler times. But here’s the thing. When it comes to building dynamic web components, vanilla JS isn’t just a nostalgic nod; it’s a powerhouse. It’s pure, unfiltered control without the noise of frameworks or libraries meddling under the hood.

I’ve been down the road of React, Vue, Angular — you name it. Sure, those tools are fantastic, but sometimes, stripping away the extras reveals the bones of how the web really works. Plus, nothing beats the feeling of creating something from scratch, watching each line of code breathe life into your app.

So, if you’re hungry to understand the core mechanics, or simply want lightweight, fast, and flexible components, vanilla JS is your friend. And trust me, the learning curve isn’t as steep as you might think.

Breaking Down What Makes a Web Component Dynamic

Before diving into code, let’s unpack what “dynamic” really means here. It’s about components that don’t just sit there looking pretty — they respond, adapt, and evolve based on user input, state changes, or other external events.

Think about a custom dropdown that opens and closes smoothly, but also fetches new options on the fly. Or a tabbed interface where clicking one tab loads different content without a page refresh. The magic sauce is in how you manage state, handle events, and update the DOM efficiently.

And yes, you can do all that without a single framework. It’s a matter of knowing which vanilla JS tools to wield.

The Anatomy of a Simple Dynamic Web Component

Let’s get hands-on. Picture this: a <color-changer> custom element that switches background colors when you click a button inside it. Simple, right? But it’s a neat little playground for key concepts.


class ColorChanger extends HTMLElement {
  constructor() {
    super();
    this.attachShadow({ mode: 'open' });
    this.colors = ['#FF6F61', '#6B5B95', '#88B04B', '#F7CAC9'];
    this.currentIndex = 0;

    this.shadowRoot.innerHTML = `
      <style>
        div {
          padding: 20px;
          text-align: center;
          background-color: ${this.colors[this.currentIndex]};
          transition: background-color 0.5s ease;
          border-radius: 8px;
          cursor: pointer;
          user-select: none;
        }
      </style>
      <div>Click me to change color</div>
    `;

    this.div = this.shadowRoot.querySelector('div');
    this.div.addEventListener('click', () => this.changeColor());
  }

  changeColor() {
    this.currentIndex = (this.currentIndex + 1) % this.colors.length;
    this.div.style.backgroundColor = this.colors[this.currentIndex];
  }
}

customElements.define('color-changer', ColorChanger);

What’s going on here? A few things worth noting:

  • Shadow DOM: We’re encapsulating styles and markup so this component doesn’t clash with the rest of the page.
  • State Management: The currentIndex tracks which color is active — a tiny, explicit state inside the component.
  • Event Handling: A click listener triggers the color change, keeping the UI dynamic and interactive.

Try dropping this into any HTML file and see it in action. It’s a neat reminder that dynamic components don’t require a heavy toolbox — just a clear mental model and some vanilla JS chops.

Going Beyond Basics: Props, Attributes, and Lifecycle Callbacks

Once you’ve played with that, the real fun begins when you start customizing your components externally. Imagine you want to pass a list of colors as an attribute or property, or respond when the component gets added or removed from the DOM.

Here’s where lifecycle callbacks like connectedCallback and attributeChangedCallback come into play. They’re your component’s way of reacting to the outside world.

For example, to observe attributes:

static get observedAttributes() { return ['colors']; }

attributeChangedCallback(name, oldValue, newValue) {
  if (name === 'colors') {
    this.colors = newValue.split(',');
    this.currentIndex = 0;
    this.updateColor();
  }
}

And updating the component’s display accordingly:

updateColor() {
  this.div.style.backgroundColor = this.colors[this.currentIndex];
}

These hooks let your component feel alive — adjusting to new data or cleanup tasks just like a well-behaved citizen of the DOM.

Performance and Best Practices: Keeping It Snappy

Here’s a nugget I’ve learned the hard way: dynamic doesn’t mean sluggish. When building components in vanilla JS, keeping performance in check is crucial.

For starters, avoid unnecessary DOM manipulations. Batch updates when possible, and consider using requestAnimationFrame for animations or visual updates. Also, clean up event listeners in disconnectedCallback to prevent memory leaks.

And while shadow DOM is fantastic for encapsulation, remember it comes with its own quirks — like style inheritance limitations. So, scope your CSS thoughtfully.

Real-World Use Case: A Dynamic FAQ Accordion

Let me walk you through a quick example I cooked up for a client: a FAQ accordion that expands and collapses answers on click — all built with vanilla JS web components.

Each question-answer pair is a custom element that manages its own open/closed state. Clicking the question toggles visibility, and keyboard accessibility is baked in for good measure.

The kicker? It was lightning-fast and easily styled to match the brand — no bulky frameworks, no complicated build steps.

Here’s a stripped-down peek:

class FaqItem extends HTMLElement {
  constructor() {
    super();
    this.attachShadow({ mode: 'open' });
    this.open = false;

    this.shadowRoot.innerHTML = `
      
        .answer { display: none; padding: 10px; }
        .answer[open] { display: block; }
        .question { cursor: pointer; padding: 10px; background: #eee; }
${this.getAttribute(‘question’)}
${this.innerHTML}

    `;

    this.question = this.shadowRoot.querySelector('.question');
    this.answer = this.shadowRoot.querySelector('.answer');

    this.question.addEventListener('click', () => this.toggle());
    this.question.addEventListener('keydown', (e) => {
      if (e.key === 'Enter' || e.key === ' ') {
        e.preventDefault();
        this.toggle();
      }
    });
  }

  toggle() {
    this.open = !this.open;
    if (this.open) {
      this.answer.setAttribute('open', '');
    } else {
      this.answer.removeAttribute('open');
    }
  }
}

customElements.define('faq-item', FaqItem);

Populating the component is as simple as:

<faq-item question="What's your return policy?">
Our return policy is 30 days, no questions asked.
</faq-item>

Simple, accessible, and effective. That’s the sweet spot I chase.

Wrapping Up (But Not Really)

So, what’s the real takeaway here? Building dynamic web components using vanilla JavaScript is not just doable, it’s downright empowering. It strips away distractions and puts you in the driver’s seat — controlling every detail, every event, every style.

And if you’re worried about it being too “bare bones,” remember: vanilla JS components can be as sophisticated and scalable as you make them. Plus, they play nicely with any framework or library you might want to layer on later.

Honestly, I wasn’t convinced at first either. But after wrestling with complex UI needs and performance headaches, vanilla JS components became my go-to toolkit. No fluff, just solid craft.

Give it a shot. Build something small, break it, rebuild it. You might just find your new favorite way to code.

So… what’s your next move?

Written by

Related Articles

Building Dynamic Web Components Using Vanilla JavaScript