Skip to content

CSS Shadow Parts

CSS Shadow Parts let a component author expose specific internal elements for external styling, going further than custom properties allow. This lesson covers the part attribute and ::part() selector.

Exposing Elements with the part Attribute

Adding a part="name" attribute to an element inside your Shadow DOM template explicitly marks it as stylable from outside the component, using the ::part(name) CSS selector from consumer stylesheets. Unlike custom properties (which expose specific *values*), parts expose entire *elements*, letting consumers apply any CSS property to that specific internal element.

This is deliberately more powerful, and more explicit, than piercing Shadow DOM with a deprecated combinator — the component author still fully controls exactly which internal elements are exposed this way, and under what names.

class ProgressBar extends LitElement {
  @property({ type: Number }) value = 0;

  render() {
    return html`
      <div part="track">
        <div part="fill" style="width: ${this.value}%"></div>
      </div>
    `;
  }
}

/* From the consumer's page-level CSS: */
progress-bar::part(track) { background: #eee; border-radius: 999px; }
progress-bar::part(fill) { background: limegreen; }

The consumer can fully restyle the track and fill elements — colors, borders, even layout — without needing any custom property the component author didn't think to expose.

part Attribute and ::part() Selector

<!-- Inside the component's Shadow DOM -->
<div part="header">...</div>

/* From outside, in the consumer's stylesheet */
my-component::part(header) {
  /* any CSS properties */
}
  • The part attribute is a native platform feature (CSS Shadow Parts spec), not something Lit invents.
  • ::part() can only be used from *outside* the component's own Shadow DOM — it doesn't work from inside the component's own static styles.
  • An element can be assigned multiple part names separated by spaces: part="button primary", then targeted as ::part(button) or ::part(primary).
  • Parts do not cascade through nested custom elements automatically — a part inside a slotted child component needs exportparts to forward it further, an advanced technique beyond this lesson's scope.

Custom Properties vs. Parts

Choosing the right theming mechanism for a given need.

Need Use
Consumer sets one specific value (a color, a radius) CSS custom property
Consumer needs full control over an internal element's styling part + ::part()
Styling top-level projected (slotted) content ::slotted()
A value that should cascade to descendants automatically CSS custom property
An element the component author wants to expose deliberately, nothing more part + ::part()

Designing Which Elements to Expose as Parts

Not every internal element should be a part — only expose elements that make sense as stable, independently-styleable pieces of your component's public API. Renaming or restructuring un-exposed internal elements later is a safe refactor; renaming or removing a documented part is a breaking change for consumers.

  • Expose structural, visually distinct pieces: a track, a fill, a header, a close button.
  • Avoid exposing implementation-detail wrapper <div>s that exist purely for internal layout.
  • Document every part name exactly like a property or event — consumers need to know what's available.

Combining Parts with Custom Properties

A well-designed component often offers both: sensible custom properties for the common theming needs (colors, spacing), plus parts for consumers who need more complete control for specific advanced cases. Most consumers use the custom properties; power users reach for ::part() when they need something the custom properties don't cover.

static styles = css`
  [part="fill"] {
    background: var(--progress-fill-color, limegreen); /* simple theming */
  }
`;
/* Power users can still fully restyle via: */
/* progress-bar::part(fill) { background: repeating-linear-gradient(...); } */

Common Mistakes

  • Trying to use ::part() from inside the component's own static styles — it only works from an external stylesheet.
  • Exposing every internal wrapper element as a part, turning routine internal refactors into breaking API changes.
  • Forgetting that parts don't automatically forward through nested custom elements without exportparts.
  • Not documenting available part names, leaving consumers to inspect the Shadow DOM manually to discover them.

Key Takeaways

  • The part attribute marks specific internal elements as styleable from outside via ::part().
  • Parts expose whole elements for full external styling, going further than the specific values custom properties expose.
  • ::part() only works from a stylesheet outside the component's own Shadow DOM.
  • Design and document your exposed parts deliberately — they become part of your component's stable public API.

Pro Tip

When in doubt about whether to expose something as a custom property or a part, start with a custom property for the specific value you know consumers need — you can always add a part later for cases that genuinely need more control, but removing an already-documented part is a breaking change.