Why can't my options.js interact with content_script.js?

Background: Hello everyone, I’m a high school student building an addon to modify font-size and color properties of our school’s website.

Structure of files:

  • /popup

    ├── options.html with 2 checkboxes (Font-Size, Change Color)

    ├── options.js to save state and retrieve value of checkboxes from localStorage.

    ├── options.css to style the popup.

  • manifest.json

  • content_script.js which targets elements on DOM with Document.getElementsByClassName() and toggles custom classnames customSize and customColor.

  • custom.css gives rules to the custom classnames.

  • icon128.png

  • icon256.png

Problem: I am able to store and retrieve boolean values of checkboxes inside the popup, but can’t send them across to content_script.js. Even using browser.tabs.executeScript().

Manually appending custom classnames from the browser console works like a charm.

Relevant code:

  1. options.js

    function restoreOptions() {
        document.getElementById("font-size").checked=JSON.parse(localStorage.getItem("size")); //assigning retrieved value to checkbox
        const executing = browser.tabs.executeScript({
            file: "/content-script.js",
            allFrames: true
        executing.then(onExecuted, onError);
    function onExecuted(result) {
    console.log(`Color changed`); }
    function onError(error) {
    console.log(`Error: ${error}`); }
  2. content_script.js

    let bigger = JSON.parse(localStorage.getItem("size"));
    if (bigger) {
    else {
  3. custom.css

    .customSize {
    font-size: 24pt;
    color: #3c8ce0;

How can I fix this?

But where do you inject the CSS file? The page won’t see your CSS file unless you inject it there.
Either manually by modifying DOM from your injected script. In that case you need to list your custom css file in the list of web-accessible resources:

Or by injecting it with:

1 Like

Hi @juraj.masiar, thanks for the suggestions. I’ll look into it right now.

To clarify though - the file custom.css is getting loaded. I know this cause there are other “universal” rules in the file (like a sepia background that the user doesn’t get option to toggle). These changes are reflected on the webpage on every reload.

Do I still need to manually inject the CSS file using web_accessible_resources? My manifest.json says

    "content_scripts": [


                    "matches": ["https://school.website/*"],

                    "js": ["content_script.js"],

                    "css": ["custom.css"]


I appreciate the help.

I don’t think so, having it in manifest for the specific host is for sure enough.
Anyway, if your CSS is loaded and your JS is indeed modifying those classes (you can inspect that easily) then the only thing that can cause issues is a lower priority of your CSS selectors. So as a proof of concept, try to add !important suffix :slight_smile:.

executeScript needs a tabId as first param if the active tab is the add-on options in the add-on manager, since it can’t run a content script in that.

1 Like

No, if the script could be injected into the current tab, then you wouldn’t need to provide the param. The issue is that about:addons can’t have content scripts at all. Though arguably, you control the contents of your options page, so you shouldn’t need a content script in the first place in it. To get another tab, tabs.query is probably the best method.

Yes, you can send a message to a content script using this. Though if you don’t know if the content script actually exists in that tab, you might want to send a message using runime.sendMessage from the content script requesting the value instead. If you are intending to have a persistent setting, storing the value in storage.local might be much better, since that can be read directly by the content script without the options having to be open.

sendMessage has nothing to do with where the script gets loaded, it is only to send an already running script a message to its runtime.onMessage listener.

You probably want to use the contentScript API for that (or have it declared in the manifest.json).

1 Like


Thank you Martin! All these little pointers really helped me get the gist of it. I re-implemented my message passing with browser.runtime.sendMessage like you recommended. Also – instead of the whole popup shenanigans, wrote a simple options.html that sits inside the addon-manager page. On testing, the classNames get added/removed just right!

Unfortunately, it only works when I have the options page open in another tab otherwise I get the error Could not establish connection. Receiving end does not exist.

ps : suppose I should mark this answered because you solved my intended question though.

that is exactly why I suggested using storage.local so settings are saved instead of having to request them from the options page.

1 Like

Oh no, I must be making a silly mistake! There is a function in my options.js to save these to local storage using :

function saveOptions(e) {
let changeSize = size.checked;
browser.storage.local.set({ changeSize }).then(setItem, onError);
let changeColor = color.checked;
browser.storage.local.set({ changeColor }).then(setItem, onError);

And I am taking changeSize and changeColor from here and passing them to the content_script using this

function handleMessage(request, sender, sendResponse) {
sendResponse({ response1: changeSize, response2: changeColor });


Kind of a facepalm moment on my part. Think you meant I can access storage.local from content_script itself?

Yes, it’s one of few APIs content scripts have direct access to: https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/Content_scripts#webextension_apis

1 Like

Ahhh…thank you!! Gonna go and try to implement this now. Will update you how it goes.

Update: It works now. Feels like I was needlessly over complicating certain parts of the code.

Thank you for bearing with me all through this, I came out much better informed and learned a lot of new things :+1:

1 Like