Content Scripts UI: Next-gen UI Injection for Web Extensions

Building user interfaces in a web extension is deceptively tricky, but it doesn't have to be with the Plasmo Framework

Louis Vilgo
Louis VilgoJuly 27, 2022
cover image for blog

Complex web extensions will require some form of user interface (UI). When you add on top a front-end library such as React or Svelte, it becomes a mess. Bundle configuring, manual DOM manipulation, and debugging seemingly trivial problems are some common issues. It is frustrating because you must keep reinventing the wheel. Not anymore with Plasmo!

Web extensions have two types of UIs:

  • Page UI, which are "webpage" specific for an extension
  • Injected UI, which are UIs mounted into a website

Extension Page UI

To use React (or any UI library) in an extension page UI such as the popup or the options page, developers generally have to do the following:

  1. Setup an HTML file that renders the extension page
  2. Create a root element in the HTML file to host the virtual DOM
  3. Create a JS/TS script file with the root element mounting logic
  4. Work on your root component
  5. Setup a bundler to compile the script above into a single file
  6. Create a script tag in the HTML file, pointing to the bundled JS script
  7. Create an entry in the manifest.json file, pointing to the HTML file
  8. (Optional) Set up TypeScript transpiling

The seven steps above must be repeated for each extension page. It becomes more complex and harder to maintain as you add more developer tools. This process demoralizes developers from starting new extension projects. It reduces experimentation and exploring what is possible!

Plasmo First-class Page UI Support

We think the manual process described above sucks. With the Plasmo framework, we squashed it down into two steps. Here is how to create the popup page:

  1. Create a popup.tsx file (or popup.vue or popup.svelte)
  2. Export default a component

Likewise, here is how to create the options page:

  1. Create an options.tsx file (or options.vue or options.svelte)
  2. Export default a component

The workflow above frees the developer from worrying about the tedious setup and focuses on what matters most - their UI component and their product.

Keen readers with front-end experience will notice that the workflow above is inspired by NextJS page routing.

Extension Injected UI

Integrating your UI with the underlying website's context is arguably one of the most powerful features of web extensions. It is, however, one of the most quirky features to implement correctly.

To illustrate how complex it can get, let's look at Toucan. Toucan is a language learning Chrome extension that randomly changes words on a site to a language you're trying to learn. A hovercard with the definition is displayed whenever the user clicks on one of the replaced words.

Toucan adding a hovercard

Before injecting UI into the webpage, we must first access its underlying DOM. Web extension has just the tool for this - "content scripts." These are essentially JavaScript files a web extension instructs the browser to run in the web page's context. With the context, developers get access to the window.document object, which enables querying, creating, and appending new DOM elements to the page.

Given the introduction above, one might think injecting UI is simple enough. All that needs to be done is to create the content script, inject a DOM element onto the target webpage, add some style with CSS, query the cursor location, add some mounting logic, and it's good to go. Right?

Well, not quite! If the DOM element and the CSS are injected directly into the website, those styles will be leaked into the underlying page. Vice versa, the styling of the website itself will also affect the UI injected by the extension.

We've heard that some developers use

[the behavior above to their advantage](

To prevent the style from leaking, developers might consider:

  • Adding a style tag with CSS unset
  • Render the element inside an iframe
  • Render the element inside a Shadow DOM

This article details each approach above and why using a shadow DOM is the recommended solution. The manual process to implement it while leveraging a UI library such as React looks like this:

  1. Create a JS/TS file and add it to the manifest.json's content_scripts array
  2. Create a shadow DOM (shDOM) host element
  3. Create a shDOM root from the host
  4. Inject the shDOM root into the webpage body
  5. Create a container element to be the root of your virtual DOM (vDOM)
  6. Append the vDOM root into the shDOM root
  7. Render the UI into the vDOM root
  8. (Optional) Set up TypeScript transpiling

Again, these seven tedious steps must be repeated every time developer wants to inject a new piece of UI into the webpage. We can abstract it into a utility helper library, but why stop there?

Introducing: Plasmo Co‎‎ntent Scripts UI

The Plasmo framework abstracted the process above into a feature we called "Content Scripts UI" or CSUI. All you have to do is:

  1. Create a content.tsx file (or contents/<name>.tsx)
  2. Export default a component

Under the hood, Plasmo wraps the component you exported inside a generated content script that implements the Shadow DOM technique above, together with many helpful features such as getMountPoint and getStyle. Developers can also override the built-in ShadowDOM container by exporting a function called getRootContainer.

From a developer's point of view, they can now focus on what matters most - the UI they want to inject into the website, not how to inject it!

For more information on this feature, please see its documentation.

💁 Note: the ".tsx" extension is essential to differentiate between Content Scripts UI and regular Content Scripts. You can replace .tsx with .svelte or .vue, and react/react-dom with the appropriate UI library in the dependencies list, and Plasmo will handle the rest.


We seek to present substantial improvements in the web extension development workflow in this article by showing the process before and after adopting the Plasmo framework. Since the release of Content Scripts UI in early June, we have enriched this feature with new UI library support and utility helpers, thus further lowering the friction when working with injected UI.

We hope our works encourage and inspire more developers to experiment with web extensions and enrich the web browsing experience.

🔥 To follow the Plasmo framework's progress, please visit our GitHub repo and give it a ⭐:


We wish you luck and magic!

Back to Blog

Thanks for reading! We're Plasmo, a company on a mission to improve browser extension development for everyone. If you're a company looking to level up your browser extension, reach out, or sign up for Itero to get started.