Skip to content

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.