Shadow DOM Styling
This lesson explains how to style Web Components inside Shadow DOM using scoped CSS, host selectors, slotted content styling, CSS custom properties, and styling hooks for themable reusable components.
What Is Shadow DOM Styling?
Shadow DOM styling means writing CSS that applies only inside a
component's shadow tree. Styles defined there do not leak out
to the page, and most global page styles do not automatically
affect internal shadow nodes.
Because encapsulation hides internal structure, Shadow DOM
styling uses special selectors and theming hooks such as
:host, ::slotted(), CSS custom
properties, and ::part().
| Concept | Description |
| Scoped CSS | Styles apply only inside the shadow tree |
:host | Styles the custom element itself |
::slotted() | Styles projected light DOM content |
| CSS Variables | Theme components from outside |
::part() | Expose selected internal elements for styling |
| Main Goal | Encapsulation with controlled customization |
Basic Scoped Styles Example
class AppButton extends HTMLElement {
constructor() {
super();
this.attachShadow({ mode: "open" });
this.shadowRoot.innerHTML = `
<style>
button {
padding: 0.5rem 1rem;
border: none;
border-radius: 0.5rem;
background: #2563eb;
color: white;
cursor: pointer;
}
button:hover {
background: #1d4ed8;
}
</style>
<button type="button">
<slot></slot>
</button>
`;
}
}
customElements.define("app-button", AppButton);
These button styles exist only inside the component. A global
page rule such as button { background: red; }
will not override the shadow button by default.
Styling the Host with :host
this.shadowRoot.innerHTML = `
<style>
:host {
display: inline-block;
}
:host([disabled]) {
opacity: 0.6;
pointer-events: none;
}
:host(.large) {
font-size: 1.125rem;
}
</style>
<button type="button"><slot></slot></button>
`;
| Selector | Purpose |
:host | Base styles for the custom element wrapper |
:host([attr]) | Style host based on attribute state |
:host(.class) | Style host when a class is present |
:host-context() | Legacy selector for ancestor-based theming |
Styling Slotted Content with ::slotted()
this.shadowRoot.innerHTML = `
<style>
::slotted(h2) {
margin: 0 0 0.5rem;
font-size: 1.25rem;
}
::slotted(p) {
color: #475569;
line-height: 1.6;
}
::slotted(a) {
color: #2563eb;
text-decoration: none;
}
</style>
<article class="card">
<slot></slot>
</article>
`;
Use ::slotted() to style content projected from the
light DOM. Regular descendant selectors such as
.card h2 do not work for slotted nodes inside Shadow
DOM.
Theming with CSS Custom Properties
this.shadowRoot.innerHTML = `
<style>
button {
background: var(--app-button-bg, #2563eb);
color: var(--app-button-color, white);
padding: var(--app-button-padding, 0.5rem 1rem);
border-radius: var(--app-button-radius, 0.5rem);
}
</style>
<button type="button"><slot></slot></button>
`;
app-button {
--app-button-bg: #059669;
--app-button-color: white;
--app-button-radius: 999px;
}
CSS custom properties pierce the Shadow DOM boundary. They are
the most common way to theme encapsulated components from the
outside without breaking encapsulation.
Exposing Internal Parts with ::part()
this.shadowRoot.innerHTML = `
<style>
.input {
width: 100%;
padding: 0.5rem 0.75rem;
border: 1px solid #cbd5e1;
border-radius: 0.5rem;
}
</style>
<label part="label">Email</label>
<input class="input" part="field" type="email" />
`;
app-text-input::part(field) {
border-color: #2563eb;
}
app-text-input::part(label) {
font-weight: 600;
}
The part attribute exposes selected internal elements
for limited external styling through ::part().
Shadow DOM Styling Strategies
| Strategy | Use When | Example |
| Internal scoped CSS | Default component appearance | button { ... } in shadow styles |
:host | Layout and host state styling | :host([open]) |
::slotted() | Projected content presentation | ::slotted(h2) |
| CSS custom properties | External theming and design tokens | --btn-bg |
::part() | Controlled styling hooks | ::part(field) |
Inherited CSS Properties
:host {
font-family: inherit;
color: inherit;
line-height: inherit;
}
Some CSS properties inherit into Shadow DOM naturally, such as
font-family, color, and
line-height. Many layout and box properties do not.
Decide intentionally which values should inherit from the page.
Variant Styling Example
class StatusBadge extends HTMLElement {
static get observedAttributes() {
return ["variant"];
}
constructor() {
super();
this.attachShadow({ mode: "open" });
this.render();
}
attributeChangedCallback() {
this.render();
}
render() {
const variant =
this.getAttribute("variant") || "info";
this.shadowRoot.innerHTML = `
<style>
span {
display: inline-block;
padding: 0.25rem 0.5rem;
border-radius: 999px;
font-size: 0.875rem;
}
.info { background: #dbeafe; color: #1e3a8a; }
.success { background: #dcfce7; color: #166534; }
.danger { background: #fee2e2; color: #991b1b; }
</style>
<span class="${variant}">
<slot></slot>
</span>
`;
}
}
Variant styling is often driven by host attributes and implemented
with internal shadow CSS classes or custom properties.
Styling From Outside vs Inside
| Method | Can Style | Cannot Style |
| Shadow internal CSS | Internal shadow nodes | External page layout outside host |
| Global page CSS | Host element and slotted light DOM | Most internal shadow nodes directly |
| CSS custom properties | Internal nodes using var() | Anything without variable hooks |
::part() | Marked internal elements only | Unmarked private internals |
Shared Styles with adoptedStyleSheets
const sheet = new CSSStyleSheet();
sheet.replaceSync(`
button {
font: inherit;
border: none;
cursor: pointer;
}
`);
class SharedButton extends HTMLElement {
constructor() {
super();
this.attachShadow({ mode: "open" });
this.shadowRoot.adoptedStyleSheets = [sheet];
this.shadowRoot.innerHTML =
`<button type="button"><slot></slot></button>`;
}
}
Constructable stylesheets let multiple components share the same
CSS efficiently, which is useful in large design systems.
When Shadow DOM Styling Helps Most
- You need reusable components safe from global CSS conflicts.
- You are building a design system with consistent internal styling.
- You want controlled theming through CSS variables and parts.
- Multiple apps with different styles consume the same components.
- You distribute widgets to third-party sites with unknown CSS.
- You need stable internals while allowing limited customization.
Common Shadow DOM Styling Use Cases
- Buttons, badges, alerts, and cards with variant styles.
- Form inputs with internal labels, borders, and focus states.
- Modals and drawers with encapsulated overlay styling.
- Tabs and accordions with private panel presentation rules.
- Themeable design tokens passed through CSS custom properties.
- Shared component CSS via adopted stylesheets.
Shadow DOM Styling Best Practices
- Keep default styles inside the shadow tree.
- Expose theming through CSS custom properties first.
- Use
::part() sparingly for intentional styling hooks. - Style slotted content with
::slotted(), not descendant selectors. - Use
:host for layout and host state styling. - Document supported variables, parts, and variant attributes.
- Decide intentionally which inherited properties should flow in.
Common Shadow DOM Styling Mistakes
- Expecting global CSS to style internal shadow nodes directly.
- Using normal descendant selectors for slotted content.
- Exposing every internal element through
part. - Hardcoding colors with no theming variables.
- Removing focus styles inside shadow CSS.
- Not documenting which customization APIs consumers should use.
- Fighting encapsulation instead of designing styling hooks upfront.
Key Takeaways
- Shadow DOM CSS is scoped to the component's internal tree.
- Use
:host for the wrapper and ::slotted() for projected content. - CSS custom properties are the main theming bridge across Shadow DOM.
::part() exposes limited styling hooks when needed. - Good Shadow DOM styling balances encapsulation with customization.
Pro Tip
Design styling in three layers: internal defaults in shadow CSS,
theme tokens through CSS custom properties, and optional
::part() hooks for advanced customization.