Styling Components: Shadow Parts


We’ve seen that CSS variables allow consumers to tweak the styles for a pre-defined property. On the flipside, there might be situations where you want to give consumers the option to completely override the CSS rules for a given element.

That’s where shadow parts come in, as they allow you as an author to define hooks that consumers can use to alter the entire CSS rule for that element.

Enabling this behaviour is straightforward, inside of your web component you can define a part attribute with a key. The consumer can now hook into the CSS rule for that part and override it to their heart’s content.

// Written by the component author
class OdysseySearch extends LitElement {
	render() {
		return html`
			<button part="search-button" @click=${this.handleClick}>
				${searchIcon}
				<p>Odyssey Search</p>
			</button>
		`;
	}
}
/* Written by the component consumer */
odyssey-search::part(search-button) {
	background-color: #cc0088;
	box-shadow: 2px 2px 0 0 #ffe0f5;
	color: white;
	padding: 16px;
}

odyssey-search::part(search-button):active {
	box-shadow: none;
	transform: translate(2px, 2px);
}

This in turn would look something like this:

Odyssey search button with overridden styles

As you can guess, this is a very powerful tool that provides heaps of control back to the consumer. If you’re building a white-labelled component library, then you’ll want to consider using shadow parts.

Exercise: Refactoring CSS variables for Shadow Parts

Let’s go ahead and refactor the Search component from having all of the CSS defined within the component itself to instead expose the DOM nodes via parts, so that our consumers can style the component.

All of the styles for the component live in search-styles.js file. Your job is to do the following:

  1. Add a part to each of the elements that we’re applying styles to, which includes:
    1. The overlay
    2. The form
    3. The input
    4. And the rest
  2. Move the CSS from the component over to the to the index.css file. Apply the styles by selecting the shadow parts in there.

You’ll run into a problem when trying to move over the [data-active] attribute, since you can’t target attribute selectors via the shadow parts.

Try and figure out a way to solve this problem. Hint, an element can have multiple parts.

Spoiler!

We can’t target attribute selectors, like [data-active] like we’ve been doing inside the shadow DOM.

So how do we solve this? Why not trying adding parts dynamically, the same way we do with the [data-active] attribute.

Targeting Nested Web Components

As we saw in a previous section, it’s possible compose a component of several smaller private web components. So what happens if you those inner web components have their own shadow parts that you want to expose to the consumer? How can your consumer target these shadow parts in a way that’s easy to understand?

There’s a newer feature in the Shadow DOM spec called exportparts which lets you re-export parts of a child component within the parent component’s namespace. Check out the following:

class WrapperComponent extends LitElement {
	render() {
		return html`
			<div part="outer">
				<inner-component exportparts="inner"></inner-component>
			</div>
		`;
	}
}

// This could be an internal component with no references in the documentation
class InnerComponent extends LitElement {
	render() {
		return html`
			<div part="inner">
				<p>Inner Component</p>
			</div>
		`;
	}
}

This means that your consumers can target the inner part as if it’s a part of the wrapper-component , like so:

complex-component::part(outer) {
  background: black;
  padding: 12px;
}

complex-component::part(inner) {
  color: white;
  font-family: "DM Sans";
}

Here’s the above in action