Search code examples
javascriptcssgoogle-chrome-extensionfirefox-addonfirefox-addon-webextensions

Can browser extensions overwrite CSS media features/query?


Can Chrome/ium or Firefox browser extensions (also called add-ons/WebExtensions) actually somehow overwrite the result of a media query?

I mean, for JS window.match​Media() this is likely easy: Just inject a content script that overwrites that JS function.

But if "real" CSS media queries (inside css files) are used, this is not really possible, is it?

I could obviously inject my own CSS, but this is not what I want: I just want to trigger the website's CSS to do something different, i.e. assume a different media query result.

Some background: If you wonder why, my use case would be to overwrite the new prefers-color-scheme CSS media feature introduced in Firefox 67. (It's not currently available for any other browsers that support browser/WebExtensions.)


Cross-posted on Mozilla's Discourse instance.


Solution

  • Workaround

    Thanks for all others, who pointed me to the right direction. Basically, there is no easy way, but you can:

    • inject a content script and iterate document.styleSheets, where all parsed style sheets are already loaded. That's the hard task. However, this is all read-only, so you cannot modify it directly.
    • Then you need to send that result back to your background script (as content scripts do not have access to the required API) and apply the CSS manually via browser.tabs.insertCSS.

    As for the first task, here is the code snippet (one time written in functional way and one time just structural) that returns all the CSS, given a media query.
    You e.g. can call getCssForMediaQueryFunc("(prefers-color-scheme: dark)") and get all the CSS applied for the dark color scheme.

    /**
     * Return CSS from the website for a specific query string.
     *
     * (functional implementation)
     *
     * @private
     * @param {string} queryString
     * @returns {string}
     */
    function getCssForMediaQueryFunc(queryString) {
        return Array.from(document.styleSheets).reduce((prev, styleSheet) => {
            /* workaround for crazy HTML spec throwing an SecurityError here,
             * see https://discourse.mozilla.org/t/accessing-some-fonts-css-style-sheet-via-stylesheet/38717?u=rugkx
             * and https://stackoverflow.com/questions/21642277/security-error-the-operation-is-insecure-in-firefox-document-stylesheets */
            try {
                styleSheet.cssRules; // eslint-disable-line no-unused-expressions
            } catch (e) {
                return prev;
            }
    
            return Array.from(styleSheet.cssRules).reduce((prev, cssRule) => {
                if (cssRule instanceof CSSMediaRule) {
                    if (cssRule.conditionText === queryString) {
                        return Array.from(cssRule.cssRules).reduce((prev, subCssRule) => {
                            return prev + subCssRule.cssText;
                        }, prev);
                    }
                }
                return prev;
            }, prev);
        }, "");
    }
    
    /**
     * Return CSS from the website for a specific query string.
     *
     * @private
     * @param {string} queryString
     * @returns {string}
     */
    function getCssForMediaQuery(queryString) { // eslint-disable-line no-unused-vars
        let cssRules = "";
        for (const styleSheet of document.styleSheets) {
            /* workaround for crazy HTML spec throwing an SecurityError here,
             * see https://discourse.mozilla.org/t/accessing-some-fonts-css-style-sheet-via-stylesheet/38717?u=rugkx
             * and https://stackoverflow.com/questions/21642277/security-error-the-operation-is-insecure-in-firefox-document-stylesheets */
            try {
                styleSheet.cssRules; // eslint-disable-line no-unused-expressions
            } catch (e) {
                continue;
            }
    
            for (const cssRule of styleSheet.cssRules) {
                if (cssRule instanceof CSSMediaRule) {
                    if (cssRule.conditionText === queryString) {
                        for (const subCssRule of cssRule.cssRules) {
                            cssRules = cssRules + subCssRule.cssText;
                        }
                    }
                }
            }
        }
        return cssRules;
    }
    

    (up-to-date code should be accessible here)

    • As mentioned before, you also need to overwrite window.match​Media() to also fake the result of the websites that could use JS for the detection. However, this is also it's own non-trivial task and requires exporting this function from the content script to the website. (Also faking that is hard.)

    Proof of concept

    I've implemented this whole thing as a more or less proof-of-concept in an add-on here, also available on addons.mozilla.org (AMO). (I've used permalinks here, but the add-on may of course be updated in the future.)

    Future

    Obviously, this is not a nice method, so I've created a new Bugzilla issue to find a better solution for that, e.g. a special Firefox API.