LEARNING OBJECTIVES โต
- Understand why CSS Shadow Parts (
::part()) were introduced to solve the limitations of CSS Custom Property theming. - Tag internal Shadow DOM elements using the
part="name1 name2"attribute. - Style internal shadow elements from outer document stylesheets using
custom-element::part(name). - Combine
::part()with pseudo-classes (:hover,:active,:focus-visible). - Forward shadow parts across nested Web Components using the
exportpartsattribute. - Know the structural limitations of
::part()(why structural descendant selectors like::part(x) > spanare prohibited).
๐ The Mental Model & Story (Intuitive Foundation)
Imagine purchasing a luxury modular car.
- Total Lockout (Early Shadow DOM without Parts): The hood is welded shut. You cannot touch or style the internal dashboard, seats, or steering wheel. If you want to change the seat color, the manufacturer must have anticipated that exact desire and exposed a specific custom property (
--car-seat-leather-color). - Complete Destruction (Piercing CSS /
/deep/- Obsolete & Deprecated): The entire chassis is made of paper. Anyone can rip into the transmission and engine components with a hacksaw, breaking all engineering safety warranties. - CSS Shadow Parts (
::part()) โ The Standard Solution: The car manufacturer installs dedicated customization ports labeledpart="steering-wheel",part="driver-seat", andpart="dashboard". You can apply any paint, fabric, or texture directly to those specific parts from the outside (my-car::part(driver-seat) { background: leather; }), but you still cannot dismantle the engine block or access internal structural sub-elements.
LIGHT DOM STYLESHEET (Outer Document)
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ user-profile::part(avatar) { border-radius: 4px; } โ
โ user-profile::part(follow-btn):hover { background: gold; } โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโฌโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ Directly styles exposed parts
โผ
SHADOW TREE (Inside #shadow-root)
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ <img part="avatar" src="avatar.png"> <--- EXPOSED PART โ
โ <div class="meta"> โ
โ <span class="user-id">#9482</span> <--- HIDDEN (Private)โ
โ </div> โ
โ <button part="follow-btn">Follow</button> <--- EXPOSED PARTโ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
Technical Deep Dive & Specifications
1. The part Attribute & ::part() Pseudo-Element
The W3C CSS Shadow Parts specification allows component developers to designate specific elements within their shadow tree as explicitly styleable by consumers.
<!-- Inside Component's Shadow DOM -->
<button part="btn confirm-action">Save Changes</button>
/* In Outer Document Stylesheet */
my-dialog::part(btn) {
padding: 10px 20px;
font-family: inherit;
}
my-dialog::part(confirm-action) {
background-color: #10b981;
color: white;
}
2. Supported Pseudo-Classes on Parts
You can append pseudo-classes to ::part() to create dynamic interactive states:
/* Interactive states */
my-dialog::part(confirm-action):hover {
background-color: #059669;
}
my-dialog::part(confirm-action):focus-visible {
outline: 2px solid #3b82f6;
outline-offset: 2px;
}
my-dialog::part(confirm-action):disabled {
opacity: 0.5;
}
3. What ::part() CANNOT Do (Architectural Guardrails)
To preserve encapsulation, the specification strictly limits selector traversal past a part:
/* โ INVALID: Cannot target descendant children of a part! */
my-dialog::part(btn) > span { color: red; }
/* โ INVALID: Cannot target adjacent siblings of a part! */
my-dialog::part(btn) + p { margin-top: 10px; }
/* โ INVALID: Cannot select arbitrary inner pseudo-elements! */
my-dialog::part(input)::placeholder { color: gray; }
4. Forwarding Nested Parts with exportparts
When you build composite Web Components (a component containing other custom elements inside its shadow root), the inner parts are hidden from the outer document by default. To expose them, use the exportparts attribute:
OUTER DOCUMENT
โ
โผ ::part(submit-btn)
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ <search-form> (Parent Custom Element) โ
โ #shadow-root โ
โ โ โ
โ โผ exportparts="button: submit-btn" โ
โ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ โ
โ โ <custom-button part="button"> (Inner Custom Element) โ โ
โ โ #shadow-root โ โ
โ โ <button part="button">Search</button> โ โ
โ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
Syntax Patterns for exportparts:
exportparts="inner-part": Exports the inner part with the same name.exportparts="inner-part: outer-name": Renames/maps the inner part to a new public name.exportparts="btn: action-btn, input: search-field": Exports multiple mapped parts separated by commas.
๐ป Interactive Code Playground
Starter Code
Save this file as css-parts.html and open it in your browser:
Line-by-Line Code Breakdown
- Line 115โ119: Inside the shadow tree, elements are decorated with
part="card-container",part="product-title",part="price-tag", andpart="buy-button". - Line 24โ47: Outer CSS document targets these parts directly (
product-card.theme-emerald::part(buy-button)), granting complete styling freedom over colors, typography, and borders. - Line 32, 47: Pseudo-classes like
:hoverare appended directly to::part(buy-button):hoverto change interaction states.
๐๏ธ Hands-On Exercise
๐ฏ The Challenge: Build a Nested Component Hierarchy with exportparts
Scenario: You are building an enterprise search bar component <search-bar> which contains an internal <icon-button> custom component. You must export the internal button's part="btn" so external consumer pages can style it using <search-bar>::part(search-btn).
Instructions:
- Create a sub-component
<icon-button>that has an internal<button part="native-btn">. - Create a parent composite component
<search-bar>containing an<input part="input-field">and<icon-button>. - On
<icon-button>, useexportparts="native-btn: search-btn"to forward and rename the part to the outer document. - In the main page CSS, customize
<search-bar>::part(input-field)with rounded pill borders and<search-bar>::part(search-btn)with an eye-catching gradient.
๐ Starter Code Sandbox
โ ๏ธ Common Pitfalls
- Attempting Descendant Selectors: Writing
my-element::part(container) pormy-element::part(card) > .titleis invalid and will be ignored by the browser parser. Parts expose only the tagged node, not its internal DOM tree. - Over-Exposing Every Single DOM Node: Do not add
part="..."to every singledivandspan. Treat parts as a deliberate design system contract, exposing only semantic UI regions (e.g.header,trigger,panel,close-btn).
๐ก Pro Tips
- Multiple Part Names: Elements can have multiple space-separated part names:
<div part="badge status-pill warning">. Consumers can target::part(badge)for general styles and::part(warning)for specific variants. - Pair Parts with Custom Properties: Use CSS Custom Properties for theme tokens (colors, fonts, radii) and CSS Parts for structural overrides (borders, layout, box-shadows).
๐ Key Takeaways
- The
part="name"attribute marks elements inside a shadow root for external styling. - The
::part(name)pseudo-element allows outer stylesheets to style tagged shadow elements without breaking encapsulation. - Pseudo-classes like
:hover,:focus, and:activecan be chained onto::part(). - Descendant selectors (
::part(x) span) are strictly prohibited by the specification. - The
exportpartsattribute forwards nested component parts through parent Web Component boundaries. - --