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.