Styling in the Shadow DOM with CSS Shadow Parts

Master the part attribute and ::part() pseudo-element to build customizable, encapsulated web components without sacrificing flexibility.

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.

Custom Button Component
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);
Button Template with part Attribute
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, :empty won't work with ::part()
  • Nested components: Parts in nested shadows need exportparts forwarding
  • Forgetting the shadow host: ::part(button) alone won't work without the element prefix
Key Capabilities

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

BrowserVersionRelease Date
Chrome/Edge73+2019
Firefox75+2020
Safari16.4+2023
Opera60+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.

Frequently Asked Questions

Build Scalable Web Components

Master modern CSS techniques like Shadow Parts to create maintainable, customizable web components for your projects. Our team of web development experts can help you build a component library that scales.