Skip to content

Web Components Handbook

A practical guide to using web components. This covers the core concepts you need to work with any web component library effectively.


The Shadow DOM

The shadow DOM is an encapsulated DOM tree attached to a custom element. It is isolated from the main document: styles defined outside do not bleed in, and styles defined inside do not leak out.

The exception is inherited CSS properties. Properties like font-family, color, and line-height inherit through the shadow boundary the same way they inherit through normal DOM nesting. A rule like this will affect text inside any component on the page:

body {
font-family: Inter, sans-serif;
color: #111;
}

This is by design. It lets components pick up your base typography without any extra configuration.

Everything else, selectors, class names, non-inherited properties, does not cross the boundary. The sections below cover what does work for theming.

To inspect the shadow DOM of a component in the browser, open DevTools and select the element. Chrome and Firefox both render the shadow root inline in the Elements panel. In some browser versions this requires enabling “Show user agent shadow DOM” in the DevTools settings first. This is useful when you need to understand the internal structure of a component before theming it.


Registration

A web component is a custom HTML element registered against a tag name using customElements.define(). Tag names must contain a hyphen, this is the browser’s way of distinguishing custom elements from native ones. This registration is global and happens once per page. Registering the same tag name twice throws an error.

// This runs once when the component module is loaded.
// Any subsequent import of the same module is a no-op.
customElements.define("my-button", MyButton);

In practice this means the component’s module should be imported once at the application level or entry point, not in every file that uses the element. After registration the tag is available everywhere in the document.


Attributes, Properties, and Methods

These work the same way they do on native HTML elements, because web components are HTML elements.

Attributes are set in HTML markup or via setAttribute. They are always strings.

<my-input placeholder="Enter your name" disabled></my-input>
el.setAttribute("placeholder", "Enter your name");
el.removeAttribute("disabled");

Boolean attributes follow the HTML convention: presence means true, absence means false. The value does not matter.

Properties are accessed via JavaScript. They can hold any type: booleans, numbers, objects, arrays.

el.value = "hello";
el.disabled = true;
console.log(el.count); // number

A component will usually keep an attribute and its corresponding property in sync, just like <input> keeps value and .value in sync.

Methods are functions exposed on the element instance, the same as calling el.focus() or el.click() on a native element.

el.open();
el.reset();

The API reference documents which attributes, properties, and methods exist, their types, and their defaults.


Slots

Slots are how you pass content into a web component. They are the equivalent of children in React or Vue’s default slot, and they work the same way as the native <slot> element in HTML templates.

Default slot

Any content placed inside the component tags that is not assigned to a named slot goes into the default slot.

<my-card>
<p>This goes into the default slot.</p>
</my-card>

Named slots

Named slots let the component define specific insertion points. You assign content to a named slot using the slot attribute.

<my-card>
<img slot="thumbnail" src="cover.jpg" alt="Cover" />
<h2 slot="title">Card Title</h2>
<p>This still goes into the default slot.</p>
</my-card>

The component controls where each slot is rendered inside its shadow DOM. You do not control the layout, only the content. The API reference lists what slots a component exposes.


Events

Web component events work exactly like native DOM events. You listen with addEventListener on the element reference or using inline event attributes.

const el = document.querySelector("my-input");
el.addEventListener("statechange", event => {
console.log(event.detail); // e.g. "opened" or "closed"
});

Some events carry a detail payload on the event object, the same as CustomEvent. The API reference documents what events a component fires and what detail contains, if anything.

Events follow normal DOM bubbling rules unless the component explicitly stops propagation. You can also listen at a parent level if the event bubbles.


Form Components

Some components in this library are form components. They accept name and disabled attributes, expose a value property, and associate with the nearest <form> element the same way native inputs do.

<form id="settings">
<my-toggle name="notifications"></my-toggle>
<button type="submit">Save</button>
</form>

They use the browser’s ElementInternals API with formAssociated = true, which means their value is included in FormData and submitted with the form without any extra work on your end.

form.addEventListener("submit", event => {
event.preventDefault();
const data = new FormData(form);
console.log(data.get("notifications")); // submitted automatically
});

Theming

Because the shadow DOM blocks external CSS selectors, theming a web component requires one of the three mechanisms below. Most components support all three.

CSS Custom Properties

CSS custom properties (variables) cross the shadow DOM boundary. A component reads variables you define on the outside, which is the primary theming mechanism.

Component libraries typically prefix their variables to avoid conflicts with your own codebase. For example, a library might use --wcp-color-primary rather than --color-primary. Check the naming convention in the API reference.

Setting a variable globally affects every instance of the component:

:root {
--color-primary: #3b82f6;
--border-radius: 4px;
}

Setting it on a specific element scopes it to that instance only:

.sidebar my-button {
--color-primary: #10b981;
}

The component’s API reference lists every CSS custom property it exposes under “CSS Properties”.

CSS Parts

::part() lets you target specific elements inside the shadow DOM directly with CSS, bypassing the encapsulation. The component author marks internal elements with a part attribute to expose them.

my-card::part(thumbnail) {
border-radius: 8px;
object-fit: cover;
}
my-card::part(title) {
font-size: 1.25rem;
font-weight: 600;
}

To know what parts a component exposes, check the “CSS Parts” section of its API reference. You can also inspect the shadow DOM in DevTools and look for part="..." attributes on internal elements.

CSS Custom States

Custom states are a way for a component to expose its internal state for CSS targeting. They work like pseudo-classes.

/* Style the component when it is in an open state */
my-accordion:state(--open) {
border-color: var(--color-primary);
}
/* Transition a part when the component enters or leaves a state */
my-accordion::part(content) {
opacity: 0;
transition: opacity 200ms ease;
}
my-accordion:state(--open)::part(content) {
opacity: 1;
}

Custom states are useful for conditional theming and for triggering animations when a component transitions between states, without needing JavaScript listeners. The API reference lists exposed states under “CSS States”.


Reading the API Reference

Every component in this library has an API reference that documents:

  • Methods — functions you can call on the element instance
  • Properties — JavaScript properties and their types
  • Attributes — HTML attributes, their accepted values, and defaults
  • Events — events the component fires and their detail payload
  • Slots — named and default slots available for content projection
  • CSS Properties — custom properties you can set to theme the component
  • CSS Parts — internal elements exposed for direct CSS targeting
  • CSS States — internal states exposed for conditional styling

When you are unsure what a component accepts or exposes, the API reference is the authoritative source. The sections in this guide map directly to those categories.