Explore Blogs
Styling shadow DOM components in react: Beyond global CSS

Customizing styles within Shadow DOM elements in React applications can be challenging due to encapsulation. This post explores effective strategies for targeting and overriding styles, ensuring your components match your application's design without directly modifying their source code.
Have you ever struggled to style a React component, only to find that your CSS rules are mysteriously ignored? This often happens when dealing with components that utilize the Shadow DOM, a powerful web standard that provides encapsulation but can also create styling headaches. Let's explore how to overcome these challenges and effectively style Shadow DOM components in your React applications.
Understanding the Shadow DOM
The Shadow DOM is a web standard that allows developers to encapsulate parts of their application, effectively hiding the internal structure and styling from the main document. This encapsulation creates a shadow boundary, isolating the component's DOM and CSS from the rest of the page. While this isolation is beneficial for preventing style conflicts and creating reusable components, it also means that standard CSS rules defined in your global stylesheet might not penetrate the shadow boundary to style elements within the Shadow DOM.
Think of it like a house within a house. The outer house is your main document, and the inner house (Shadow DOM) has its own interior design (styles) that are not directly affected by the outer house's decor. To change the interior of the inner house, you need specific tools and methods.
Why use Shadow DOM in React?
While React itself provides component-level encapsulation, the Shadow DOM offers an additional layer of isolation. This can be particularly useful in the following scenarios:
- Component Libraries: When building a component library, you want to ensure that your component's styles don't conflict with the styles of the applications where they are used. Shadow DOM helps achieve this by encapsulating the component's styling.
- Third-Party Components: If you're incorporating components from external sources, Shadow DOM can prevent their styles from interfering with your application's styles, and vice versa.
- Web Components: Shadow DOM is a fundamental part of the Web Components standard, allowing you to create truly reusable and encapsulated components that can be used in any web application, regardless of the framework.
Strategies for styling Shadow DOM components
Now that we understand the Shadow DOM and its benefits, let's explore different strategies for styling components that use it within a React application:
CSS variables (custom properties)
CSS variables, also known as custom properties, provide a powerful way to pass styling information from the main document to the Shadow DOM. You can define CSS variables in the parent scope and then use them within the Shadow DOM to style elements. This approach allows for a degree of customization without directly targeting elements inside the shadow tree. CSS variables are inherited, meaning that if a variable is not defined in the shadow DOM, it will inherit from the parent. This provides a mechanism for global styles to influence the appearance of components rendered with Shadow DOM.
Example:
// React Component (Parent)
function MyComponent() {
return (
<div style={{ '--primary-color': 'blue' }}>
<ShadowComponent />
</div>
);
}
// Shadow Component (Child - using Shadow DOM)
class ShadowComponent extends React.Component {
constructor(props) {
super(props);
this.shadowRef = React.createRef();
}
componentDidMount() {
this.shadow = this.shadowRef.current.attachShadow({ mode: 'open' });
// Create a style element
const style = document.createElement('style');
style.textContent = `
.text {
color: var(--primary-color, black); // Use the CSS variable, default to black if not set
}
`;
this.shadow.appendChild(style);
// Create a div element
const div = document.createElement('div');
div.className = 'text';
div.textContent = 'This is in Shadow DOM';
this.shadow.appendChild(div);
}
render() {
return <div ref={this.shadowRef}></div>;
}
}
In this example, the --primary-color
CSS variable is defined in the parent component's style. The ShadowComponent
then uses this variable to set the color of the text within its Shadow DOM. If the variable is not defined in the parent, the text color defaults to black. Default value prevents application crash issues due to undefined variables.
::part
pseudo-element
The ::part
pseudo-element allows you to style specific elements within a Shadow DOM from the outside. For this to work, the component author needs to explicitly expose these elements using the part
attribute in the component's template. This is a more targeted approach than CSS variables, as it allows you to style specific parts of the component without affecting other elements within the Shadow DOM.
Example:
// Shadow Component (defines parts)
class ShadowComponent extends React.Component {
constructor(props) {
super(props);
this.shadowRef = React.createRef();
}
componentDidMount() {
this.shadow = this.shadowRef.current.attachShadow({ mode: 'open' });
// Create a style element
const style = document.createElement('style');
style.textContent = `
.container {
background-color: lightgray;
padding: 10px;
}
`;
this.shadow.appendChild(style);
// Create a div element with a "part" attribute
const div = document.createElement('div');
div.className = 'container';
div.setAttribute('part', 'container'); // Expose this element as a part
div.textContent = 'This is a styled container in Shadow DOM';
this.shadow.appendChild(div);
}
render() {
return <div ref={this.shadowRef}></div>;
}
}
// React Component (Parent) - styling the part
function MyComponent() {
return (
<div>
<ShadowComponent />
<style>
shadow-component::part(container) {
border: 2px solid red;
}
</style>
</div>
);
}
In this example, the ShadowComponent
exposes its container element as a part
named "container". The parent component then uses the ::part(container)
selector to add a red border to this container. Note that you have to use the component's tag name (in this case shadow-component
- assuming you registered it as a custom element) to target the part.
::theme
pseudo-element (experimental)
The ::theme
pseudo-element (still experimental and not widely supported) is similar to ::part
but provides a more semantic way to style components. Instead of targeting specific elements, you target conceptual "themes" defined by the component. This requires the component author to define which themes are available and how they affect the component's appearance. If the component supports the ::theme
pseudo-element, it will provide a way to style conceptual themes rather than specific parts. This requires the component author to define which themes are available and how they affect the component's appearance.
Example:
// Shadow Component (defines themes - example)
class ShadowComponent extends React.Component {
constructor(props) {
super(props);
this.shadowRef = React.createRef();
}
componentDidMount() {
this.shadow = this.shadowRef.current.attachShadow({ mode: 'open' });
// Create a style element
const style = document.createElement('style');
style.textContent = `
.button {
padding: 10px 20px;
border: none;
color: white;
}
.button.primary {
background-color: blue;
}
.button.secondary {
background-color: gray;
}
`;
this.shadow.appendChild(style);
// Create a button element
const button = document.createElement('button');
button.className = 'button primary'; // Default theme
button.textContent = 'Click Me';
this.shadow.appendChild(button);
}
render() {
return <div ref={this.shadowRef}></div>;
}
}
// React Component (Parent) - styling the theme - if supported
function MyComponent() {
return (
<div>
<ShadowComponent />
<style>
shadow-component::theme(secondary) {
background-color: green; /* Overrides the default */
}
</style>
</div>
);
}
In this hypothetical example, the ShadowComponent
defines two themes: "primary" and "secondary". The parent component attempts to use ::theme(secondary)
to change the background color of the button when the "secondary" theme is applied. Since ::theme
is not yet widely supported, this example is primarily for illustrative purposes. Also, note that React doesn't natively support the ::theme
pseudo-element, as it is still experimental. This example demonstrates how it *could* be used if it were supported.
Injecting stylesheets into the Shadow DOM
Another approach is to directly inject a stylesheet into the Shadow DOM using JavaScript. This gives you complete control over the styling within the Shadow DOM, but it also requires more code. This method involves creating a <style>
element, adding your CSS rules to it, and then appending it to the Shadow DOM. This is particularly useful when you need to apply complex styles or dynamically generate styles based on component state.
Example:
// Shadow Component (injecting styles)
class ShadowComponent extends React.Component {
constructor(props) {
super(props);
this.shadowRef = React.createRef();
}
componentDidMount() {
this.shadow = this.shadowRef.current.attachShadow({ mode: 'open' });
// Create a style element
const style = document.createElement('style');
style.textContent = `
.text {
font-size: 18px;
font-family: sans-serif;
}
`;
this.shadow.appendChild(style);
// Create a div element
const div = document.createElement('div');
div.className = 'text';
div.textContent = 'Dynamically styled Shadow DOM content';
this.shadow.appendChild(div);
}
render() {
return <div ref={this.shadowRef}></div>;
}
}
// React Component (Parent)
function MyComponent() {
return (
<div>
<ShadowComponent />
</div>
);
}
In this example, the ShadowComponent
creates a <style>
element, adds CSS rules to it, and then appends it to the Shadow DOM. This ensures that the text within the Shadow DOM is styled according to the specified rules. Remember that any styles defined in the main document will not affect this content, as it is isolated within the Shadow DOM.
CSS Shadow Parts
CSS Shadow Parts are a set of CSS properties that allow you to define specific parts of a web component's shadow DOM that can be styled from the outside. This allows you to create web components that are highly customizable, while still maintaining the benefits of encapsulation. This allows for more granular control over the styling of web components from the outside. Using Shadow Parts, a web component can expose specific elements within its shadow DOM to be styled by the host page, enabling customization while preserving encapsulation.
Example:
// Shadow Component (defines parts)
class ShadowComponent extends React.Component {
constructor(props) {
super(props);
this.shadowRef = React.createRef();
}
componentDidMount() {
this.shadow = this.shadowRef.current.attachShadow({ mode: 'open' });
// Create a style element
const style = document.createElement('style');
style.textContent = `
.container {
background-color: lightgray;
padding: 10px;
}
`;
this.shadow.appendChild(style);
// Create a div element with a "part" attribute
const div = document.createElement('div');
div.className = 'container';
div.setAttribute('part', 'container'); // Expose this element as a part
div.textContent = 'This is a styled container in Shadow DOM';
this.shadow.appendChild(div);
}
render() {
return <div ref={this.shadowRef}></div>;
}
}
// React Component (Parent) - styling the part
function MyComponent() {
return (
<div>
<ShadowComponent />
<style>
shadow-component::part(container) {
border: 2px solid red;
}
</style>
</div>
);
}
In this example, the container within the shadow DOM is exposed with the part
attribute set to container
. The parent component then styles this part using the ::part
pseudo-element.
Important considerations
- Specificity: CSS specificity rules still apply within the Shadow DOM. If a style rule within the Shadow DOM has higher specificity than a rule defined outside, it will take precedence.
- Accessibility: Ensure that your styling choices don't negatively impact the accessibility of your components. Pay attention to color contrast, font sizes, and other factors that affect usability for users with disabilities.
- Performance: Be mindful of the performance implications of injecting stylesheets or manipulating the DOM within the Shadow DOM. Optimize your code to minimize reflows and repaints.
- Component Design: When designing components that use Shadow DOM, consider providing a well-defined set of CSS variables or parts that allow consumers to customize the component's appearance without needing to dive into the internal implementation details.
Choosing the right strategy
The best strategy for styling Shadow DOM components depends on your specific needs and the level of control you require:
- CSS Variables: Good for simple theming and providing a limited set of customization options.
::part
: Offers more targeted styling of specific elements within the Shadow DOM.::theme
: (Experimental) Provides a semantic way to style components based on predefined themes.- Injecting Stylesheets: Gives you the most control but requires more code and can be less maintainable.
- CSS Shadow Parts: Good for exposing shadow DOM elements for external styling.