Shadow DOM Styling
Scoped styles that can't leak. CSS custom properties that cross the boundary. Theming without naming conventions.
Scoped Styles
Styles inside a <template> are scoped to the component. They affect the shadow DOM contents and nothing else. This is enforced by the browser — not a build tool, not a convention.
<template id="my-button">
<style>
/* This .label class ONLY exists inside this component */
.label {
font-weight: bold;
text-transform: uppercase;
color: white;
}
button {
all: unset;
cursor: pointer;
padding: 0.5rem 1rem;
border-radius: 4px;
background: var(--accent-primary);
}
</style>
<button>
<span class="label"><slot>Click me</slot></span>
</button>
</template>
Inside Shadow DOM, .label won't collide with any other .label on the page. You can use simple, meaningful class names without BEM prefixes or CSS Modules.
The :host Selector
The :host selector targets the custom element itself (the outer element in the light DOM). Use it to set the element's display type, dimensions, and default styles.
:host {
display: block;
padding: 1rem;
border: 1px solid var(--border-color);
border-radius: 8px;
}
/* Contextual :host — matches when the host has specific attributes */
:host([type="success"]) {
border-color: #00cc66;
}
:host([type="error"]) {
border-color: #cc0000;
}
:host([disabled]) {
opacity: 0.5;
pointer-events: none;
}
/* :host-context matches based on an ancestor */
:host-context(.dark-theme) {
background: rgba(0, 0, 0, 0.8);
}
| Selector | What It Matches |
|---|---|
:host |
The custom element itself |
:host([attr]) |
The element when it has the given attribute |
:host([attr="value"]) |
The element when the attribute matches a specific value |
:host(.class) |
The element when it has the given class |
:host(:hover) |
The element on hover (or any pseudo-class) |
CSS Custom Properties
CSS custom properties (variables) are the one thing that deliberately crosses the shadow boundary. This is by design — it's how components participate in a page's theme without breaking encapsulation.
<style>
:root {
--accent-primary: #e85d26;
--accent-secondary: #d4a574;
--text-primary: #c8c0b8;
--border-color: rgba(200, 192, 184, 0.15);
--surface-bg-rgb: 40, 36, 32;
}
</style>
:host {
color: var(--text-primary);
border: 1px solid var(--border-color);
background: rgba(var(--surface-bg-rgb), 0.3);
}
.title {
color: var(--accent-primary);
}
Always provide a fallback for CSS variables your component uses: color: var(--accent-primary, #e85d26). This makes the component usable even without a theme.
::slotted() Selector
The ::slotted() pseudo-element lets you style elements that are slotted into your component from the light DOM. It only targets top-level slotted elements — not their descendants.
/* Style all slotted elements */
::slotted(*) {
margin-bottom: 0.5rem;
}
/* Style specific slotted elements */
::slotted(h1) {
font-size: 2rem;
color: var(--text-bright);
}
::slotted(p) {
color: var(--text-secondary);
line-height: 1.6;
}
/* Style slotted elements with specific attributes */
::slotted([slot="title"]) {
font-weight: bold;
}
::slotted() only targets the direct slotted element, not children within it. ::slotted(div > span) will not work. If you need to style deeper elements, use CSS custom properties or have the slotted content include its own styles.
Material Symbols Integration
Shadow DOM blocks external stylesheets, including icon fonts. If your component needs Material Symbols, you must declare the font-family inside the shadow styles or import the stylesheet:
.icon {
font-family: "Material Symbols Outlined";
font-size: 24px;
font-weight: normal;
font-style: normal;
line-height: 1;
letter-spacing: normal;
text-transform: none;
white-space: nowrap;
word-wrap: normal;
direction: ltr;
-webkit-font-smoothing: antialiased;
}
@import url('/shadow-component/google_icons.css');
Then use the icons in your template markup:
<span class="icon">check_circle</span> <span class="icon">settings</span> <span class="icon">close</span>
Theming Strategy
The recommended approach for themeable components:
- Use CSS custom properties for all colors, spacing, and typography — these cross the shadow boundary
- Provide fallback values so the component works standalone
- Use
:host([attr])for variant styling (e.g.,type="primary",size="large") - Expose component-specific variables for fine-grained control
:host {
/* Component-specific variables with fallbacks to global theme */
--card-bg: var(--surface-bg, rgba(40, 36, 32, 0.3));
--card-border: var(--border-color, rgba(200, 192, 184, 0.15));
--card-radius: var(--radius-md, 8px);
--card-padding: var(--spacing-md, 1rem);
display: block;
background: var(--card-bg);
border: 1px solid var(--card-border);
border-radius: var(--card-radius);
padding: var(--card-padding);
}
/* Consumers can override just the component variables */
/* <my-card style="--card-bg: navy; --card-radius: 0;"> */
Prefix component-specific variables with the component name: --card-bg, --card-border. This avoids collisions when consumers set variables on a parent element that contains multiple component types.