Understanding Shadow DOM Styling Challenges
Shadow DOM revolutionized how we build web components by providing encapsulation--styles defined inside a component won't bleed out and external styles won't accidentally break the component's appearance. This isolation is powerful but creates a challenge: how do you give component consumers the flexibility to customize parts of your component when you can't expose the internal DOM structure?
CSS Shadow Parts provide the answer. The part attribute and ::part() pseudo-element create a controlled API for styling shadow DOM internals, enabling customization without sacrificing encapsulation.
For custom elements to be fully useful and as capable as built-in elements, it should be possible for parts of them to be styled from outside while giving component authors control over exactly what can be styled from outside.
The part Attribute: Exposing Elements
The part attribute is a global HTML attribute that marks elements inside a shadow tree as stylable from outside. When you add part to an element, you're essentially creating a public API for that component.
Basic Syntax
<template id="my-element-template">
<style>
.button {
background: blue;
color: white;
padding: 0.5rem 1rem;
}
</style>
<button class="button" part="submit-button">
<slot></slot>
</button>
</template>
The part="submit-button" makes this button accessible to external stylesheets via ::part(submit-button).
Multiple Part Names
Elements can have multiple part names, just like CSS classes:
<div part="header main-content">Both parts are exposed</div>
<!-- Order doesn't matter -->
<div part="main-content header">Also valid</div>
Unlike CSS classes, part names are declared in HTML attributes and cannot be changed via JavaScript at runtime. They create a stable API surface for component customization.
The ::part() Pseudo-Element: Selecting and Styling
The ::part() pseudo-element selects elements within a shadow tree that have been exposed via the part attribute. It only works on shadow hosts--elements with shadow DOM attached.
Basic Usage
/* Select the submit-button part within my-element */
my-element::part(submit-button) {
background: #0066cc;
color: white;
font-weight: bold;
}
/* Select elements with both part names */
my-element::part(header main) {
border-bottom: 1px solid #ccc;
}
Combining with Pseudo-Classes
Standard pseudo-classes work with ::part():
my-element::part(button):hover {
background: #004499;
}
my-element::part(button):focus {
outline: 2px solid #0066cc;
outline-offset: 2px;
}
Pseudo-Element Chaining Restrictions
You cannot chain multiple ::part() pseudo-elements--my-element::part(button)::part(label) will never match anything. This restriction prevents exposing more structure than intended. Instead, use multiple part names on a single element and select with ::part(button label) for intersection matching.
For additional context, see the MDN ::part() reference and the W3C CSS Shadow Parts specification.
1class StylableButton extends HTMLElement {2 constructor() {3 super();4 const template = document.getElementById('button-template');5 this.attachShadow({ mode: 'open' })6 .appendChild(template.content.cloneNode(true));7 }8}9customElements.define('stylable-button', StylableButton);1<template id="button-template">2 <style>3 :host {4 display: inline-block;5 }6 .button {7 padding: 0.75rem 1.5rem;8 border: none;9 border-radius: 4px;10 cursor: pointer;11 font-family: inherit;12 transition: all 0.2s ease;13 }14 </style>15 <button class="button" part="button">16 <slot></slot>17 </button>18</template>exportparts: Forwarding Nested Parts
When components nest, parts inside inner components aren't visible to outer CSS. The exportparts attribute forwards parts through the shadow boundary, enabling composable component architectures. For teams building modern web applications with component libraries, this pattern enables clean separation between base components and their wrappers.
Basic Syntax
<!-- Inner component -->
<template id="inner-button-template">
<button part="inner-label">Click me</button>
</template>
<!-- Outer component using exportparts -->
<template id="outer-template">
<inner-button exportparts="inner-label: button-label">
</inner-button>
</template>
Now external styles can target the forwarded part:
inner-button::part(button-label) {
color: blue;
}
Mapping Pseudo-Elements
Pseudo-elements can also be exported:
<div part="message" exportparts="::before: alert-icon">
<slot></slot>
</div>
<!-- Styling the exported pseudo-element -->
alert-component::part(alert-icon)::before {
content: "⚠️ ";
}
This pattern is essential for building design systems where base components need to expose styling hooks to parent components that wrap them.
Best Practices
For Component Authors
Expose Minimal Surface Area
- Only mark elements as parts that you intend to be customized
- Don't expose internal implementation details
- Think of parts as a public API
Provide Documentation
- Document which parts are available
- Explain what each part controls
- Note any CSS properties that won't work due to encapsulation
Use Consistent Naming
- Choose clear, semantic names for parts
- Be consistent across related components
- Consider a naming convention for variants
For Component Consumers
Target Specific Components
/* Good: specific */
my-button::part(icon) { }
/* Avoid: too generic */
::part(icon) { }
Understand Inheritance
- Some styles inherit through the shadow boundary
- Others need explicit targeting via parts
- Test thoroughly across browsers
Common Pitfalls
- Wrong selector:
::part(btn):part(primary)doesn't work--use::part(btn primary) - Structural pseudo-classes:
:first-child,:emptywon't work with::part() - Nested components: Parts in nested shadows need
exportpartsforwarding - Forgetting the shadow host:
::part(button)alone won't work without the element prefix
Encapsulation
Maintain component isolation while enabling controlled customization through opt-in part exposure.
Composition
Use exportparts to forward parts through nested component hierarchies for composable architectures.
Theming
Combine with CSS custom properties for dynamic, theme-aware component styling that adapts to design systems.
Browser Support
| Browser | Version | Release Date |
|---|---|---|
| Chrome/Edge | 73+ | 2019 |
| Firefox | 75+ | 2020 |
| Safari | 16.4+ | 2023 |
| Opera | 60+ | 2019 |
As of 2025, CSS Shadow Parts have 96%+ global support, making them safe to use in production for most applications.
Feature Detection
if (HTMLElement.prototype.part !== undefined) {
console.log('Shadow Parts are supported');
}
Performance Considerations
Shadow Parts have minimal runtime performance impact. CSS selector matching for ::part() is optimized by browsers, and style recalculation is localized to affected parts. The part element map is calculated once and cached, with no significant performance penalty compared to regular CSS selectors.
Group related part styles together and use CSS custom properties for theming when possible to minimize style recalculations.