What Are Web Components? The Browser-Native UI Building Blocks AI Loves to Generate
You asked Claude to build a reusable card component and instead of React, it gave you something called a "custom element" with "shadow DOM." Here's what that means and why it might be exactly what you need.
TL;DR
Web Components are browser-native custom HTML elements — like <user-card> or <price-tag> — that you define yourself. They combine three browser APIs: Custom Elements (invent your own tags), Shadow DOM (isolate styles so nothing leaks), and HTML Templates (reusable markup). They work without React, Vue, or any framework. AI tools love generating them because the output is a single file that runs in any browser — no build step, no npm install, no dependencies. If you're building something lightweight and reusable, Web Components might be the better choice.
Why AI Coders Need to Understand Web Components
Here's a pattern you'll run into sooner than you think: You ask your AI to build a reusable UI piece — a testimonial card, a star rating widget, a pricing tier — and instead of handing you a React component with useState and JSX, it gives you something like this:
<user-card name="Chuck" role="Builder"></user-card>
That's not a typo. That's not a framework. That's a Web Component — a custom HTML tag that the browser understands natively once you define it with JavaScript.
AI tools are increasingly reaching for Web Components because:
- Zero dependencies. No React, no Vue, no Svelte, no build tools. The code works the moment you open the HTML file.
- One file, done. AI can give you a single HTML file with the component definition and usage in one place.
- Style isolation for free. Shadow DOM means the component's CSS won't break your page layout and your page layout won't break the component.
- Works everywhere. Drop a Web Component into a React app, a WordPress page, a static site, or a plain HTML file. It doesn't care.
Understanding the basics — even at a high level — means you can read what AI generates, know when it's the right tool, debug it when something looks wrong, and tell your AI partner when you'd rather have a React component instead.
Real Scenario: Building a Reusable Info Card
"Build me a reusable card component I can use across my site. It should show a person's name, their role, and a short bio. I want to use it like a custom HTML tag — no React, no build step. Just something I can drop into any HTML page."
This is exactly the kind of prompt where AI will reach for Web Components. You said "reusable," "custom HTML tag," and "no React." That's Web Components territory.
What AI Generated
Here's what Claude might hand you. Don't worry about understanding every line right now — we'll break it all down in the next section.
<!-- STEP 1: Define the component -->
<script>
class UserCard extends HTMLElement {
constructor() {
super()
// Create a shadow DOM — a private, isolated DOM tree
const shadow = this.attachShadow({ mode: 'open' })
// Define the component's internal HTML and styles
shadow.innerHTML = `
<style>
.card {
border: 1px solid #e2e8f0;
border-radius: 12px;
padding: 24px;
max-width: 320px;
font-family: 'Inter', sans-serif;
background: #ffffff;
}
.name {
font-size: 1.25rem;
font-weight: 700;
margin: 0 0 4px 0;
color: #1a202c;
}
.role {
font-size: 0.875rem;
color: #718096;
margin: 0 0 12px 0;
}
.bio {
font-size: 0.95rem;
line-height: 1.6;
color: #4a5568;
}
</style>
<div class="card">
<p class="name"></p>
<p class="role"></p>
<div class="bio">
<slot>No bio provided.</slot>
</div>
</div>
`
}
// connectedCallback runs when the element is added to the page
connectedCallback() {
const shadow = this.shadowRoot
shadow.querySelector('.name').textContent = this.getAttribute('name') || 'Unknown'
shadow.querySelector('.role').textContent = this.getAttribute('role') || ''
}
}
// Register the custom element with the browser
customElements.define('user-card', UserCard)
</script>
<!-- STEP 2: Use it like any HTML tag -->
<user-card name="Chuck Kile" role="Builder & AI-Enabled Coder">
20 years in construction, now building software with AI.
No CS degree. No bootcamp. Just curiosity and Claude.
</user-card>
<user-card name="Sarah Chen" role="Designer & Prompt Engineer">
Former graphic designer who discovered she could build
entire web apps by describing them to AI.
</user-card>
Two things to notice right away: you use <user-card> like a normal HTML tag, and each instance gets its own data through attributes and inner content. That's the magic of Web Components.
Understanding Each Part
Web Components are built on three browser APIs that work together. Think of them as three tools in a toolbox — you don't always need all three, but they're designed to complement each other.
Custom Elements: Inventing Your Own HTML Tags
Custom Elements let you define brand-new HTML tags. Instead of building everything with <div>s and classes, you create semantic, meaningful tags like <user-card>, <price-table>, or <nav-drawer>.
There's one rule: custom element names must contain a hyphen. That's how the browser tells them apart from built-in HTML tags. <usercard> won't work. <user-card> will.
// Define a class that extends HTMLElement
class UserCard extends HTMLElement {
constructor() {
super() // Always call super() first
// Set up your component here
}
connectedCallback() {
// Runs when the element is added to the page
// This is where you read attributes and render content
}
disconnectedCallback() {
// Runs when the element is removed from the page
// Clean up event listeners, timers, etc.
}
}
// Tell the browser about your new tag
customElements.define('user-card', UserCard)
After that customElements.define() call, the browser knows what <user-card> means. Every time it encounters one in your HTML, it creates an instance of your UserCard class.
connectedCallback() method is where the action happens — it runs when your element appears on the page. Think of it like React's useEffect on mount, but built into the browser.
Shadow DOM: The Protective Bubble for Your Styles
This is the part that sounds intimidating but is actually the most useful feature of Web Components. Here's the plain English version:
Shadow DOM is a private room for your component's HTML and CSS.
Without Shadow DOM, every CSS rule on your page is global. If you write .card { padding: 20px; } in your main stylesheet, every element with the class "card" gets that padding — including your component's internals. And if your component defines .card { background: red; }, that leaks out and turns every card on the page red.
Shadow DOM fixes this. When you call this.attachShadow({ mode: 'open' }), you create an isolated DOM tree. Styles inside don't get out. Styles outside don't get in.
// Without Shadow DOM:
// Your component's .card styles affect the ENTIRE page
// The page's .card styles affect YOUR component
// Everything fights everything. Chaos.
// With Shadow DOM:
const shadow = this.attachShadow({ mode: 'open' })
shadow.innerHTML = `
<style>
/* These styles ONLY apply inside this component */
.card { background: white; padding: 24px; }
p { color: #333; margin: 0; }
</style>
<div class="card">
<p>I'm safe in my bubble.</p>
</div>
`
// The page's styles can't touch this.
// This component's styles can't leak out.
Why this matters for you: if you're building something to drop into multiple pages — or multiple projects — Shadow DOM guarantees your component will look the same everywhere. No CSS conflicts. No surprises.
Slots: Letting Users Put Content Inside Your Component
Slots are how you let people put their own content inside your custom element. Remember this from the example?
<user-card name="Chuck" role="Builder">
20 years in construction, now building software with AI.
</user-card>
That text between the tags? It gets projected into the component wherever you put a <slot> tag:
// Inside the shadow DOM:
<div class="bio">
<slot>No bio provided.</slot>
</div>
The <slot> tag is a placeholder. Content the user puts between the custom element's tags fills the slot. If they don't provide any content, the fallback text ("No bio provided.") shows instead.
You can even have named slots for multiple content areas:
// Component definition (inside shadow DOM):
<div class="card">
<div class="header"><slot name="title">Default Title</slot></div>
<div class="body"><slot>Default content</slot></div>
<div class="footer"><slot name="actions"></slot></div>
</div>
// Usage:
<my-card>
<span slot="title">Getting Started</span>
<p>This goes into the default (unnamed) slot.</p>
<button slot="actions">Learn More</button>
</my-card>
If you've used React, slots are like children props — but with the ability to have multiple named insertion points.
HTML Templates: Reusable Markup Blueprints
The <template> element holds HTML that isn't rendered when the page loads. It's a blueprint you can stamp out copies of in JavaScript:
<template id="card-template">
<style>
.card { border: 1px solid #e2e8f0; padding: 16px; border-radius: 8px; }
</style>
<div class="card">
<h3></h3>
<p></p>
</div>
</template>
<script>
class InfoCard extends HTMLElement {
constructor() {
super()
const shadow = this.attachShadow({ mode: 'open' })
// Clone the template — don't modify the original
const template = document.getElementById('card-template')
const content = template.content.cloneNode(true)
// Fill in the data
content.querySelector('h3').textContent = this.getAttribute('title')
content.querySelector('p').textContent = this.getAttribute('desc')
shadow.appendChild(content)
}
}
customElements.define('info-card', InfoCard)
</script>
In practice, AI often skips the <template> element and just sets shadow.innerHTML directly. Both approaches work. Templates are slightly more performant when you're creating many instances because the browser can optimize cloning.
Attributes: Passing Data to Your Component
Just like built-in HTML tags accept attributes (<img src="photo.jpg" alt="A photo">), your custom elements can read attributes too:
<user-card name="Chuck" role="Builder"></user-card>
Inside your component, read them with this.getAttribute('name'). You can also watch for attribute changes:
class UserCard extends HTMLElement {
// Tell the browser which attributes to watch
static get observedAttributes() {
return ['name', 'role']
}
// Runs whenever a watched attribute changes
attributeChangedCallback(attrName, oldValue, newValue) {
if (attrName === 'name') {
this.shadowRoot.querySelector('.name').textContent = newValue
}
}
}
This is how Web Components handle reactivity — when an attribute changes, attributeChangedCallback fires and you update the DOM. It's more manual than React's automatic re-renders, but it's happening directly in the browser with zero overhead.
Web Components vs. React: When to Use Which
This is the question you'll actually need to answer. Here's the honest breakdown:
| Scenario | Web Components | React/Vue/Svelte |
|---|---|---|
| Single reusable widget (card, badge, tooltip) | ✅ Perfect fit | Overkill |
| Static site with a few interactive pieces | ✅ No build step needed | Might work but adds complexity |
| Full single-page application | Possible but painful | ✅ Built for this |
| Complex state management (auth, cart, filters) | Manual and verbose | ✅ Mature ecosystem |
| Drop-in component for any website | ✅ Framework-agnostic | Requires React on the host page |
| Team project with established stack | Depends on the stack | ✅ More developers know it |
| Quick AI-generated prototype | ✅ Single file, zero setup | Needs project scaffolding |
What AI Gets Wrong About Web Components
1. Forgetting connectedCallback and Using constructor Alone
AI sometimes puts attribute reading inside the constructor, where attributes might not be available yet:
// FRAGILE: Attributes may not be set when constructor runs
class BadCard extends HTMLElement {
constructor() {
super()
const shadow = this.attachShadow({ mode: 'open' })
// This might return null if the attribute isn't set yet!
shadow.innerHTML = `<h2>${this.getAttribute('title')}</h2>`
}
}
// BETTER: Read attributes in connectedCallback
class GoodCard extends HTMLElement {
constructor() {
super()
this.attachShadow({ mode: 'open' })
}
connectedCallback() {
this.shadowRoot.innerHTML = `<h2>${this.getAttribute('title')}</h2>`
}
}
2. Not Handling Missing Attributes
AI often assumes every attribute will be provided. Real usage is messier:
// AI writes this:
shadow.querySelector('.name').textContent = this.getAttribute('name')
// If name attribute is missing, this shows "null" as text
// Better:
shadow.querySelector('.name').textContent = this.getAttribute('name') || 'Anonymous'
3. Generating Overly Complex Components
Sometimes AI will create a Web Component for something that should just be plain HTML and CSS. A card that doesn't need isolation, interactivity, or reuse? Just use a <div> with a class. Not everything needs to be a component.
4. Ignoring Accessibility
AI-generated Web Components often skip ARIA attributes, keyboard navigation, and focus management. Shadow DOM can actually complicate accessibility because screen readers need to traverse the shadow boundary. Always ask your AI: "Make this component accessible — add ARIA labels, keyboard support, and proper focus management."
5. Styles That Look Wrong Because of Shadow DOM Isolation
Your page's global font, color variables, or CSS reset won't automatically apply inside Shadow DOM. AI might generate a component that looks perfect in isolation but clashes with your site because it's using its own default fonts:
// Inside shadow DOM, inherit from the page:
:host {
font-family: inherit; /* Use the page's font */
color: inherit; /* Use the page's text color */
}
// Or use CSS custom properties (variables) that pierce the shadow boundary:
:host {
color: var(--text-color, #333); /* Falls back to #333 if --text-color isn't set */
}
How to Debug Web Component Problems with AI
Problem: Custom Element Shows as Empty or Plain Text
"My custom element <user-card> renders as an empty box. The JavaScript class is defined but nothing shows up inside it. Here's my code: [paste code]. Check if customElements.define() is being called, if the tag name matches, and if connectedCallback is setting the innerHTML correctly."
Problem: Component Styles Don't Match the Rest of the Page
"My Web Component uses Shadow DOM and the fonts/colors don't match my site. The component looks like it's ignoring my global CSS. How do I make it inherit my page's font-family and use my CSS custom properties for colors?"
Problem: Attribute Changes Don't Update the Component
"I'm changing a Web Component's attribute with JavaScript (element.setAttribute('name', 'New Name')) but the displayed text doesn't update. Here's my component: [paste code]. I think I need observedAttributes and attributeChangedCallback — can you add them?"
Inspecting Shadow DOM in DevTools
Open your browser's DevTools (F12), find your custom element in the Elements panel, and you'll see a #shadow-root node you can expand. This shows the component's internal HTML. You can inspect and edit styles inside the shadow root just like regular elements.
A Complete, Production-Ready Example
Here's a Web Component you can actually use — a notification banner with different types (info, warning, success, error), a dismiss button, and proper accessibility:
class AlertBanner extends HTMLElement {
constructor() {
super()
this.attachShadow({ mode: 'open' })
}
connectedCallback() {
const type = this.getAttribute('type') || 'info'
const dismissible = this.hasAttribute('dismissible')
this.shadowRoot.innerHTML = `
<style>
:host { display: block; margin: 16px 0; }
:host([hidden]) { display: none; }
.banner {
padding: 16px 20px;
border-radius: 8px;
display: flex;
align-items: flex-start;
gap: 12px;
font-family: inherit;
line-height: 1.5;
}
.banner.info { background: #ebf8ff; border-left: 4px solid #4299e1; color: #2b6cb0; }
.banner.warning { background: #fffaf0; border-left: 4px solid #ed8936; color: #c05621; }
.banner.success { background: #f0fff4; border-left: 4px solid #48bb78; color: #276749; }
.banner.error { background: #fff5f5; border-left: 4px solid #fc8181; color: #c53030; }
.content { flex: 1; }
.dismiss {
background: none; border: none; cursor: pointer;
font-size: 1.25rem; line-height: 1; padding: 0;
color: inherit; opacity: 0.6;
}
.dismiss:hover { opacity: 1; }
</style>
<div class="banner ${type}" role="alert">
<div class="content"><slot></slot></div>
${dismissible ? '<button class="dismiss" aria-label="Dismiss">×</button>' : ''}
</div>
`
if (dismissible) {
this.shadowRoot.querySelector('.dismiss').addEventListener('click', () => {
this.setAttribute('hidden', '')
this.dispatchEvent(new CustomEvent('dismissed'))
})
}
}
}
customElements.define('alert-banner', AlertBanner)
// Usage:
// <alert-banner type="success" dismissible>
// Your changes have been saved!
// </alert-banner>
//
// <alert-banner type="warning">
// Your API key expires in 3 days.
// </alert-banner>
What to Learn Next
Frequently Asked Questions
What are Web Components?
Web Components are a set of browser-native APIs that let you create reusable custom HTML elements with encapsulated styles and behavior. They consist of three main technologies: Custom Elements (define your own tags like <user-card>), Shadow DOM (isolate styles so they don't leak in or out), and HTML Templates (reusable markup blueprints). They work in every modern browser — Chrome, Firefox, Safari, Edge — without any framework or build tool.
What is Shadow DOM in plain English?
Shadow DOM is like a protective bubble around your component's HTML and CSS. Styles inside the shadow DOM can't affect the rest of your page, and styles from the rest of your page can't affect your component. Think of it as each component having its own private stylesheet that lives in isolation. This means you can use simple class names like .card or .title without worrying about conflicts with the rest of your site.
Should I use Web Components or React?
Use Web Components for standalone, reusable UI pieces that need to work anywhere — across different frameworks, in plain HTML pages, or in projects where you don't want a build step. Use React (or Vue/Svelte) when building a full interactive application with complex state management, routing, and data flow. They're not competitors — they solve different problems. You can even use Web Components inside a React app.
Why does AI generate Web Components instead of React components?
AI generates Web Components when you ask for something lightweight, reusable, and framework-free. Since Web Components use native browser APIs, the AI doesn't need to assume you have React installed, a bundler configured, or any dependencies available. The generated code is a single file that runs in any browser — open it and it works. This makes them ideal for quick prototypes, widgets, and projects where simplicity matters.
Do Web Components work in all browsers?
Yes. As of 2024, Custom Elements, Shadow DOM, and HTML Templates are fully supported in all modern browsers — Chrome, Firefox, Safari, and Edge. You no longer need polyfills. The only browsers that don't support them are truly legacy ones like Internet Explorer, which Microsoft no longer supports. If your users have a browser from the last 3–4 years, Web Components will work.