DEV Community

Cover image for Solving CSS and JavaScript Interference in Chrome Extensions: A Guide to React, Shadow DOM, and Best Practices
Thomas Sarpong
Thomas Sarpong

Posted on

Solving CSS and JavaScript Interference in Chrome Extensions: A Guide to React, Shadow DOM, and Best Practices

Introduction

Chrome extensions are powerful tools that enhance user experiences by integrating seamlessly with existing web applications. However, one of the challenges developers often encounter is CSS and JavaScript interference between the extension and the host web page. This article explores a few approaches to mitigate these issues, with a focus on leveraging React, Shadow DOM, and best practices for building robust and non-intrusive Chrome extensions.

Background

In a recent project, I worked on a Chrome extension that needed to render a full application via a content script on an existing DOM. The client wanted the extension to feel like a natural part of the existing web application, similar to how extensions like Grammarly or Word Tune integrate. The UI was complex, requiring advanced state management and frequent re-rendering. To handle this, I chose to leverage React, Webpack, Babel, and Redux RTK for state management.

Here's how we initially set up the extension:

import { createRoot } from 'react-dom';
import './style.scss';

function App() {
    return <div>Extension App</div>;
}

export function render() {
    const rootElement = document.createElement('div');
    rootElement.id = 'extension-app-root';
    document.body.appendChild(rootElement);

    const reactRoot = createRoot(rootElement);
    reactRoot.render(<App />);
}
Enter fullscreen mode Exit fullscreen mode

While this solution seemed to work well at first, we quickly realized that the styles and JavaScript from the different apps were interfering with each other, causing unexpected behavior.

Potential Solutions

After some research, we identified three potential solutions to address the interference issues:

Solution 1: Class Name Prefixing

One approach was to prefix all our class names with a unique identifier to differentiate them from the parent DOM. While this helped resolve some styling conflicts, it did not fully address global style resets or JavaScript interference from the parent document.

Pros:

  • Simple to implement.
  • Solves some styling conflicts.

Cons:

  • Doesnโ€™t address all global style resets.
  • Does not prevent JavaScript interference.

Solution 2: Rendering Content in an iframe

Another solution was to write the React app as a standalone web application, deploy it, and load it in an iframe rendered by the content script.

const iframeElement = document.createElement('iframe');
iframeElement.src = 'https://www.linktoreactapp.com';
document.body.appendChild(iframeElement);    
Enter fullscreen mode Exit fullscreen mode

This approach encapsulates both styles and JavaScript within the iframe, but it introduces communication challenges between the extension and the iframe since the chrome.runtime API is not available in the iframe.

Messages can be posted using the iframe.contentWindow API:

iframeElement.contentWindow.postMessage('hello', 'https://www.linttoreactapp.com');
Enter fullscreen mode Exit fullscreen mode

Messages in the iframe can be received with:

window.onmessage = function(e) {
    if (e.data === 'hello') {
        alert('It works!');
    }
};
Enter fullscreen mode Exit fullscreen mode

For more details, see the MDN documentation.

Pros:

  • Prevents style conflicts with the parent document.
  • Isolates JavaScript execution.

Cons:

  • Requires complex communication management between the iframe and the extension.
  • Potential latency issues and security vulnerabilities.
  • Increases the overall complexity of the extension.

Note: This was the solution we implemented for the company, and it worked well for our use case.

Solution 3: Using Shadow DOM

Shadow DOM is a more elegant solution that allows for style and JavaScript encapsulation. This method ensures that styles and scripts from the parent document do not interfere with the extension and vice versa.

Hereโ€™s how the original solution was modified to use a Shadow DOM:

import { createRoot } from 'react-dom';
import './style.scss';

function App() {
    return <div>Extension App</div>;
}

export function render() {
    // Create an element and attach a shadow root to it
    const shadowRootElement = document.createElement('div');
    const shadowRoot = shadowRootElement.attachShadow({ mode: 'open' });

    // Append the shadow root to the body of the parent document
    document.body.appendChild(shadowRootElement);

    // Create a root element and attach it to the shadow root
    const rootElement = document.createElement('div');
    rootElement.id = 'extension-app-root';

    // Append the react root element to the shadow root
    shadowRoot.appendChild(rootElement);

    // Create a react root and render the app
    const reactRoot = createRoot(rootElement);
    reactRoot.render(<App />);
}
Enter fullscreen mode Exit fullscreen mode

Pros:

  • Provides full encapsulation of styles and scripts.
  • Simplifies maintenance by preventing conflicts.

Cons:

  • Potential issues with styling not being applied due to encapsulation.

Best Solution and Best Practices

While class name prefixing is a simple solution, it does not address all styling and JavaScript interference issues. iframe encapsulation is effective but introduces communication challenges and potential performance issues due to network latency. This leaves Shadow DOM as the best solution for providing perfect encapsulation and avoiding data communication problems.

Potential Styling Issues and Solutions with the Shadow DOM Technique

Problem: Styles Not Applied

The encapsulation provided by Shadow DOM prevents styles and JavaScript applied in the parent head, script, or style tags from affecting elements inside the Shadow DOM. This can cause CSS styles not to apply to the targeted elements.

Solution: Use a JavaScript Implementation of CSS

  • Inline Styles: You can use inline styles in JSX, but this is limited and doesnโ€™t support core CSS features like :hover, :before, and other pseudo-elements.
  • Emotion with CacheProvider: A more powerful solution is to use Emotion, scoped under a CacheProvider. This ensures that styles are applied within the Shadow DOM without affecting the parent document.
import { createRoot } from 'react-dom';
import createCache from '@emotion/cache';
import { CacheProvider } from '@emotion/react';

function App() {
    return <div>Extension App</div>;
}

export function render() {
    // Create an element and attach a shadow root to it
    const shadowRootElement = document.createElement('div');
    const shadowRoot = shadowRootElement.attachShadow({ mode: 'open' });

    // Append the shadow root to the body of the parent document
    document.body.appendChild(shadowRootElement);

    // Create a root element and attach it to the shadow root
    const rootElement = document.createElement('div');
    rootElement.id = 'extension-app-root';

    // Append the react root element to the shadow root
    shadowRoot.appendChild(rootElement);

    // Create a react root and render the app
    const reactRoot = createRoot(rootElement);

    // Create an Emotion cache scoped to the Shadow DOM
    const emotionCache = createCache({
        key: 'extension-app',
        prepend: true,
        container: rootElement,
        speedy: true,
    });

    reactRoot.render(
        <CacheProvider value={emotionCache}>
            <App />
        </CacheProvider>
    );
}
Enter fullscreen mode Exit fullscreen mode

Conclusion

In conclusion, while each solution has its strengths, Shadow DOM stands out as the most robust option for avoiding interference between your extension and the host application. For simple extensions, class name prefixing might suffice, but for more complex scenarios, especially where full encapsulation is needed, Shadow DOM is the recommended approach. Choose the solution that best fits your needs, and consider the trade-offs in terms of complexity and performance.

Further Reading

Top comments (0)