Web Components API Reference
This lesson is a practical API reference for Web Components standards, covering Custom Elements, Shadow DOM, templates, slots, events, and styling hooks with examples and quick lookup tables.
Web Components APIs at a Glance
Web Components are built on browser-native APIs. This reference
summarizes the most important interfaces and methods you use when
creating, styling, composing, and communicating with custom
elements in production projects.
| Standard | Main API | Purpose |
| Custom Elements | customElements, HTMLElement | Define and upgrade custom tags |
| Shadow DOM | attachShadow(), shadowRoot | Encapsulate internal DOM and CSS |
| HTML Templates | <template> | Store reusable inert markup |
| Slots | <slot> | Project light DOM content |
| Events | CustomEvent | Emit component output |
| Forms | ElementInternals | Participate in forms and validation |
Custom Elements API
class AppBadge extends HTMLElement {
static get observedAttributes() {
return ["variant"];
}
constructor() {
super();
}
connectedCallback() {
this.render();
}
attributeChangedCallback(name, oldValue, newValue) {
if (oldValue !== newValue) this.render();
}
}
customElements.define("app-badge", AppBadge);
| API | Description |
customElements.define(name, class) | Registers a custom element class |
customElements.get(name) | Returns the constructor for a defined element |
customElements.whenDefined(name) | Promise that resolves when the element is defined |
customElements.upgrade(root) | Upgrades undefined custom elements in a tree |
HTMLElement.observedAttributes | Static list of attributes to watch |
connectedCallback() | Called when element is added to the DOM |
disconnectedCallback() | Called when element is removed from the DOM |
attributeChangedCallback() | Called when an observed attribute changes |
adoptedCallback() | Called when element moves to a new document |
Attributes and Properties API
class ToggleSwitch extends HTMLElement {
static get observedAttributes() {
return ["checked"];
}
get checked() {
return this.hasAttribute("checked");
}
set checked(value) {
this.toggleAttribute("checked", Boolean(value));
}
attributeChangedCallback(name) {
if (name === "checked") {
this.dispatchEvent(
new Event("change", { bubbles: true })
);
}
}
}
| Method / Property | Description |
getAttribute(name) | Read an attribute value as a string |
setAttribute(name, value) | Set or update an attribute |
hasAttribute(name) | Check whether an attribute exists |
removeAttribute(name) | Remove an attribute |
toggleAttribute(name, force) | Add or remove a boolean attribute |
element.property | JavaScript property API for complex values |
Use attributes for declarative HTML configuration and properties
for rich JavaScript values such as objects, arrays, and functions.
Shadow DOM API
class ProfileCard extends HTMLElement {
constructor() {
super();
this.shadow =
this.attachShadow({ mode: "open" });
this.shadow.innerHTML = `
<style>
:host { display: block; }
.card { padding: 1rem; border: 1px solid #ccc; }
</style>
<article class="card" part="card">
<slot name="title"></slot>
<slot></slot>
</article>
`;
}
}
| API | Description |
element.attachShadow({ mode }) | Creates a shadow root on the host element |
element.shadowRoot | Returns the open shadow root, if available |
mode: "open" | Shadow root accessible via shadowRoot |
mode: "closed" | Shadow root hidden from outside access |
:host | Styles the custom element host from inside shadow CSS |
::slotted() | Styles content assigned to a slot |
part / ::part() | Expose selected internal elements for styling |
HTML Template API
<template id="user-row-template">
<article class="row">
<strong><slot name="name"></slot></strong>
<span><slot name="role"></slot></span>
</article>
</template>
class UserRow extends HTMLElement {
connectedCallback() {
const template =
document.getElementById("user-row-template");
const content =
template.content.cloneNode(true);
this.appendChild(content);
}
}
| API | Description |
<template> | Holds inert markup that is not rendered immediately |
template.content | DocumentFragment containing template children |
cloneNode(true) | Creates a reusable copy of template content |
Slots API
<info-card>
<h2 slot="title">Billing</h2>
<p>Your plan renews tomorrow.</p>
<button slot="footer">Manage</button>
</info-card>
class InfoCard extends HTMLElement {
connectedCallback() {
this.innerHTML = `
<section>
<header><slot name="title"></slot></header>
<div><slot></slot></div>
<footer><slot name="footer"></slot></footer>
</section>
`;
this.querySelector("slot").addEventListener(
"slotchange",
() => this.handleSlotChange()
);
}
}
| API | Description |
<slot> | Default slot for projected child content |
<slot name="..."> | Named slot for targeted content |
slot="name" | Assigns an element to a named slot |
slot.assignedElements() | Returns elements assigned to a slot |
slotchange | Event fired when slotted content changes |
Custom Events API
this.dispatchEvent(
new CustomEvent("item-selected", {
detail: {
id: this.itemId,
label: this.label
},
bubbles: true,
composed: true,
cancelable: true
})
);
| Option | Description |
detail | Payload attached to the event object |
bubbles: true | Event propagates up the DOM tree |
composed: true | Event crosses Shadow DOM boundaries |
cancelable: true | Event can be canceled with preventDefault() |
event.detail | Data read by parent app listeners |
ElementInternals API
class AppTextInput extends HTMLElement {
static formAssociated = true;
constructor() {
super();
this.internals = this.attachInternals();
}
connectedCallback() {
this.innerHTML = `<input type="text" />`;
this.input = this.querySelector("input");
this.input.addEventListener("input", () => {
this.internals.setFormValue(this.input.value);
});
}
}
| API | Description |
static formAssociated = true | Marks the element as form-associated |
attachInternals() | Gets the ElementInternals object |
internals.setFormValue(value) | Sets the control value for form submission |
internals.setValidity() | Reports validation state to the form |
internals.form | Returns the associated form, if any |
Styling APIs
app-button {
--btn-bg: #2563eb;
--btn-color: white;
}
app-button::part(button) {
border-radius: 0.5rem;
}
const sheet = new CSSStyleSheet();
sheet.replaceSync(`
button { background: var(--btn-bg, #111); }
`);
this.shadowRoot.adoptedStyleSheets = [sheet];
| API | Description |
| CSS custom properties | Theme components from outside the shadow tree |
part attribute | Marks internal elements for external styling |
::part(name) | Styles exposed internal parts from outside |
adoptedStyleSheets | Shares constructable stylesheets in Shadow DOM |
:host-context() | Applies styles based on outer context |
Declarative Shadow DOM
<profile-card>
<template shadowrootmode="open">
<style>
:host { display: block; }
</style>
<article><slot></slot></article>
</template>
<p>Alex Chen</p>
</profile-card>
Declarative Shadow DOM lets you define a shadow tree directly in
HTML. This is useful for server-side rendering and progressive
enhancement without waiting for JavaScript to attach a shadow root.
Quick Reference by Task
| Task | API to Use |
| Register a custom tag | customElements.define() |
| React to DOM insertion | connectedCallback() |
| Watch attribute changes | observedAttributes + attributeChangedCallback() |
| Encapsulate markup and CSS | attachShadow() |
| Accept external content | <slot> |
| Notify parent apps | CustomEvent |
| Participate in forms | ElementInternals |
| Theme from outside | CSS custom properties and ::part() |
When to Use Each API
- Use
customElements.define() for every reusable custom tag. - Use Shadow DOM when internal structure and CSS should stay private.
- Use templates when markup should be cloned instead of rebuilt as strings.
- Use slots when consumers need flexible composition.
- Use custom events when parent apps need output from a component.
- Use ElementInternals for custom form controls and validation.
- Use CSS parts and custom properties when theming must stay controlled.
API Usage Best Practices
- Keep the public API small: attributes, properties, events, and slots.
- Document every observed attribute and emitted event.
- Prefer open shadow roots unless strong encapsulation is required.
- Use
composed: true only when parent apps must hear the event. - Sync attributes and properties predictably for framework consumers.
- Clean up listeners and timers in
disconnectedCallback(). - Expose styling hooks intentionally instead of leaking all internals.
Common API Mistakes
- Calling
attachShadow() more than once on the same element. - Forgetting to declare
observedAttributes before watching changes. - Using attributes for object values instead of properties.
- Emitting events without documenting names and detail shape.
- Overusing closed Shadow DOM and breaking testing or theming.
- Not using
composed: true for events that must leave Shadow DOM. - Exposing too many internal parts and defeating encapsulation.
Key Takeaways
- Web Components are powered by a small set of browser-native APIs.
- Custom Elements handle behavior, Shadow DOM handles encapsulation.
- Templates and slots enable reusable and composable markup.
- Custom events and ElementInternals extend the public contract.
- This reference is the foundation for building production-ready components.
Pro Tip
When documenting a component library, list its API in four sections:
attributes, properties, events, and slots. That matches how developers
actually consume Web Components in HTML and JavaScript.