How to really isolate stylesheets in the Google Chrome extension?
Asked Answered
S

5

38

I wrote a Google Chrome extension, which popups a dialog with an autocomplete field and it's own style, but there are some sites where my CSS gets totally broken, which doesn't look very nice.

I know about isolating styles with iFrames, but in Google Chrome extension there is no way to isolate my HTML and CSS in this way. Another method is to wrap all my stuff into a separated div with it's own id and relative styles for that id, and I do so, but it seems that it doesn't work on some sites with "hard" tags style overloading or "!important" directives in the CSS code.

So, I want to know is there any way to really isolate my styles in z convenient way or it's my bad carma to overload every little CSS property to fix one or another style issue for each site?

By the way: I set up my manifest to load all the things at the "document_end", but I see it's not being applied to the stylesheets which is every time loaded whenever the DOM is ready.

Shanonshanta answered 8/10, 2012 at 13:56 Comment(4)
Have you already tried these methods? A combination of both should result be sufficient. Make sure that your selectors are sufficiently specific.Latchkey
Thanks Rob! Yeah, I tried this, but it's seems doesn't work for every site. I know pages where even if I see that "Computed style" (in GC DevTools) shows all as I set it in my CSS, like "list-style: none !important;", for example, it's still continue to display me the list with 'disc' markers. And, yes — my styles setted up in manifest like in your advice and it's loaded dynamicaly by embedding to the head after DOM was created. Maybe there are some another way to isolate styles in Google Chrome extensions? I mean some secret 'disable-inheritance: on' option in manifest or something like that?Shanonshanta
How about my last suggestion? Increase the specifity of your selectors. If you control the elements which you want to style, adding style="prop:val!important" will have the highest precedence - for example see jsfiddle.net/rytkL.Latchkey
See my answer to a similar question here: #10609424. There are a couple approaches, but the one I detail is perhaps easiest and most reliable... :)Stairs
L
24

At the time of asking the question, your only option was to either use iframes, or stylesheets with a very high specificity and explicitly set all properties that might affect styles. The last method is very cumbersome, because there will always be some property that is overlooked by you. Consequently, the only usable method for isolating stylesheets was to use iframes.

The solution to this problem -isolation of styles without iframes- is Shadow DOM (since Chrome 25). You can find a tutorial at HTML5 Rocks. For a real-world Chrome extension that uses Shadow DOM to isolate styles, see Display #Anchors (source code here).

Latchkey answered 27/11, 2013 at 11:29 Comment(4)
That Display #Anchors extension comes in really handy (e.g. for linking to specific sections of the Chrome extenions docs ;))Tetrachloride
also using Sass like: #myid { .all-my-extension-styles ... } could potentially solve this problem?Weise
@rob-w your answer seems little complicated. Can you give the simplified code to inject an element and override it in the top of other elements?Lentic
I was able to create a shadowDOM element but it is getting overlayed by the page once loaded. For example in Medium editor it is displaying the shadow element for a second and it is hiding. I was able to inspect the element in dev tools.Lentic
D
17

As I've recently gone through the gauntlet of this issue, I want to share some information I think is valuable.

First, Rob W's answer is correct. Shadow DOM is the correct solution to this problem. However, in my case not only did I need CSS isolation, I also needed JavaScript events. For example, what happens if the user clicks a button that lives within the isolated HTML? This gets really ugly with just Shadow DOM, but we have another Web Components technology, Custom Elements, to the rescue. Except that as of this writing there is a bug in chrome that prevents custom element in chrome extensions. See my questions here and here and the bug here.

So where does that leave us? I believe the best solution today is IFrames, which is what I went with. The article shahalpk linked is great but it only describes part of the process. Here's how I did it:

First, create an html file and js file for your isolated widget. Everything inside these files will run in an isolated environment in an iframe. Be sure to source your js file from the html file.

//iframe.js
var button = document.querySelector('.my-button');
button.addEventListener('click', function() {
    // do useful things
});

//iframe.html
<style>
/* css */
</style>
<button class='my-button'>Hi there</button>
<script src='iframe.js'></script> 

Next, inside your content script create an iframe element in javascript. You need to do it in javascript because you have to use chrome.extension.getURL in order to grab your iframe html file:

var iframe = document.createElement('iframe');
iframe.src = chrome.extension.getURL("iframe.html");
document.body.appendChild(iframe);

And that's it.

One thing to keep in mind: If you need to communicated between the iframe and the rest of the content script, you need to chrome.runtime.sendMessage() to the background page, and then chrome.tabs.sendMessage from the background page back to the tab. They can't communicate directly.

EDIT: I wrote a blog post detailing everything I learned through my process, including a complete example chrome extension and lots of links to different information:

https://apitman.com/3/#chrome-extension-content-script-stylesheet-isolation

In case my blog goes down, here's the sources to the original post:

Blog post

Example source

Dorisdorisa answered 3/8, 2014 at 2:2 Comment(1)
BTW as mentioned in that blog post you may want to look into my chromeps library to handle the message passing. This blog post explains itDorisdorisa
C
9

Either use all

.some-selector {
    all: initial;
}

.some-selector * {
    all: unset;
}

or use Shadow DOM

Library

function Widget(nodeName, appendTo){
  this.outer = document.createElement(nodeName || 'DIV');
  this.outer.className = 'extension-widget-' + chrome.runtime.id;
  this.inner = this.outer.createShadowRoot();
  (appendTo || document.body).appendChild(this.outer);
}

Widget.prototype.show = function(){
  this.outer.style.display = 'block';
  return this;
};

Widget.prototype.hide = function(){
  this.outer.style.display = 'none';
  return this;
};

Usage

var myWidget = new Widget();
myWidget.inner.innerHTML = '<h1>myWidget</h1>';

You can access the widget contents via myWidget.inner and the outer via myWidget.outer.

Styles

/* 
 * Reset Widget Wrapper Element 
 */
.extension-widget-__MSG_@@extension_id__ {
  background: none;
  border: none;
  bottom: auto;
  box-shadow: none;
  color: black;
  cursor: auto;
  display: inline;
  float: none;
  font-family : "Helvetica Neue", "Helvetica", "Arial", sans-serif;
  font-size: inherit;
  font-style: normal;
  font-variant: normal;
  font-weight: normal;
  height: auto;
  left: auto;
  letter-spacing: 0;
  line-height: 100%;
  margin: 0;
  max-height: none;
  max-width: none;
  min-height: 0;
  min-width: 0;
  opacity: 1;
  padding: 0;
  position: static;
  right: auto;
  text-align: left;
  text-decoration: none;
  text-indent: 0;
  text-shadow: none;
  text-transform: none;
  top: auto;
  vertical-align: baseline;
  white-space: normal;
  width: auto;
  z-index: 2147483648;
}

/* 
 * Add your own styles here 
 * but always prefix them with:
 * 
 *   .extension-widget-__MSG_@@extension_id__ 
 *   
 */

.extension-widget-__MSG_@@extension_id__{
  position: fixed;
  top: 100px;
  margin: 0 auto;
  left: 0;
  right: 0;
  width: 500px;
}

.extension-widget-__MSG_@@extension_id__::shadow h1 {
  display: block;
  margin: 0 auto;
  padding: 20px;
  background-color: yellow;
  border: 10px solid green;
  font-size: 20px;
  text-align: center;
}
Carry answered 3/3, 2015 at 12:39 Comment(3)
Shadow DOM is very tricky when it comes to event handling. I am trying to use React + Shadow DOM to isolate the extension, and still failing to bind the event handler to inner DOM.Smelt
I have done exactly the same. In the end I realized that react and shadow dom doesn't work well together. The good old iframe is still the best option when it comes to encapsulation.Carry
Good to hear somebody out there is with me. But it seems if you're using iframe, the message passing between the host and the iframe content can be quite difficult. How you manage to do that?Smelt
S
5

I recently created Boundary, a CSS+JS library to solve problems just like this. Boundary creates elements that are completely separate from the existing webpage's CSS.

Take creating a dialog for example. After installing Boundary, you can do this in your content script

var dialog = Boundary.createBox("yourDialogID", "yourDialogClassName");

Boundary.loadBoxCSS("#yourDialogID", "style-for-elems-in-dialog.css");

Boundary.appendToBox(
    "#yourDialogID",
    "<button id='submit_button'>submit</button>"
);

Boundary.find("#submit_button").click(function() {
  // some js after button is clicked.
});

Elements within #yourDialogID will not be affected by the existing webpage. And find() function returns a regular jQuery DOM element so you can do whatever you want with it.

Hope this helps. Please let me know if you have any question.

https://github.com/liviavinci/Boundary

Sever answered 17/10, 2014 at 18:54 Comment(3)
Good! Now this is a much better answer. (Though you should take out the snippet tag, it's not useful)Lagting
@Lagting this is the first time I've ever posted an answer on stackoverflow so thanks a lot for the guidance!Sever
Does this respond well to user events?Soracco
A
2

Use iframes. It's a workaround, but works fine.

Maxime has written an article on it.

Agouti answered 27/11, 2013 at 8:25 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.