Styling Shadow DOM with the CSS ::part() Pseudo-Element

Web Components rely on the Shadow DOM to encapsulate internal markup and styles, preventing external CSS rules from unintentionally leaking inside. The CSS ::part() pseudo-element bridges this encapsulation gap in an intentional, controlled manner by allowing component authors to expose specific internal elements to external styling via a designated attribute. This article explains how the part attribute and ::part() selector function together, the syntax required to use them, the restrictions in place to preserve component encapsulation, and how to handle nested shadow trees using exportparts.

The Encapsulation Barrier and the part Attribute

By default, standard CSS selectors declared in an external stylesheet cannot select nodes inside a component's shadow root. While CSS custom properties (variables) allow for the passing of individual values (like colors or font sizes), they do not permit comprehensive rule declarations.

To provide controlled styling access, the component author explicitly decorates internal shadow DOM elements with the HTML part attribute:

<!-- Inside custom-card's shadow root -->
<div class="card-container">
  <h2 part="title">Card Title</h2>
  <p part="content">Card description text goes here.</p>
  <button part="action-btn">Read More</button>
</div>

The values assigned to the part attribute act as named styling hooks. Elements without a part attribute remain completely hidden from external styling rules. An element can also specify multiple space-separated parts (e.g., part="action-btn primary"), allowing it to match multiple target selectors.

Targeting Exposed Nodes with ::part()

From an external stylesheet, consumers target the exposed element by appending ::part(name) to the custom element's tag name or class:

custom-card::part(title) {
  color: #1a202c;
  font-size: 1.5rem;
}

custom-card::part(action-btn) {
  background-color: #0066cc;
  color: #ffffff;
  border-radius: 4px;
}

The browser evaluates the selector by matching the custom element host first, looking into its shadow tree, and applying the defined declaration block directly to any element marked with that specific part identifier.

Supported Pseudo-Classes and Structural Restrictions

The ::part() pseudo-element supports standard pseudo-classes, allowing external authors to style interactive states:

custom-card::part(action-btn):hover {
  background-color: #004999;
}

custom-card::part(action-btn):focus-visible {
  outline: 2px solid #ffbf47;
}

However, to maintain encapsulation integrity, the specification prohibits descending into or selecting child elements through the part selector. A selector like custom-card::part(content) span or custom-card::part(content) > p is invalid and will be ignored by the browser. Only the element with the part attribute itself can be styled.

Forwarding Parts in Nested Shadow Trees with exportparts

If a custom element nests another custom element inside its shadow DOM, the inner component's parts are not automatically visible to the outer document. To make an inner part accessible to the global scope, the outer component must explicitly forward it using the exportparts attribute:

<!-- Inside parent-component's shadow root -->
<nested-button exportparts="btn: action-btn"></nested-button>

This maps the inner component's btn part to the outer name action-btn. An external stylesheet can now reach the deeply nested button using parent-component::part(action-btn).

Summary

The ::part() pseudo-element establishes an explicit styling contract between component creators and consumers. By assigning part attributes to internal nodes, authors choose exactly what can be modified, granting users the freedom to write comprehensive CSS rules for designated elements without risking the breakage of internal layout and structure.