Search code examples
chrome-extension-manifest-v3

How to get OS dark/light setting in Chrome Extension MV3 background js?


In MV2, it's quite easy because there's a background.html concept, so that I can access window.matchMedia('(prefers-color-scheme: dark)').matches to understand the OS's theme setting.

But in MV3, the background.html is gone and in the background.js, if you retrieve that value, it will display the following error:

> window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches; 
Uncaught ReferenceError: window is not defined
    at <anonymous>:1:1
(anonymous) @.....

Is there anything I can do?

The reason I want to achieve this is to set a observer in the background script to check the OS theme changing, so that I can update the extension icon accordingly to make sure it's perfectly matching the OS style.

Thanks for any kind of tips.


Solution

  • 1. Make the initial check using the offscreen document:

    manifest.json:

      "permissions": ["offscreen", "storage"],
    

    background.js

    (async () => {
      await chrome.offscreen.createDocument({
        url: 'offscreen.html',
        reasons: ['MATCH_MEDIA'],
        justification: '!',
      }).catch(() => {});
      setDarkTheme(await chrome.runtime.sendMessage('checkDarkTheme'));
      chrome.offscreen.closeDocument();
    })();
    
    chrome.runtime.onMessage.addListener((msg, sender, sendResponse) => {
      if (msg === 'darkThemeChanged') {
        setDarkTheme(msg.data);
      }
    });
    
    function setDarkTheme(val) {
      chrome.storage.session.set({isDark: val});
      // do something meaningful e.g. change the icon 
    }
    

    offscreen.html:

    <script src=offscreen.js></script>
    

    offscreen.js:

    chrome.runtime.onMessage.addListener((msg, sender, sendResponse) => {
      if (msg === 'checkDarkTheme') {
        sendResponse(matchMedia('(prefers-color-scheme: dark)').matches);
      }
    });
    

    2. Observe the change in a script for a visible page

    Browsers don't report it in invisible pages like MV2 background page or the offscreen document. This means that you won't receive any notification until a visible page is opened.

    You'll need to declare a content script for <all_urls>, which adds "Reads data on all sites" permission warning when installing the extension.

    content.js:

    {
      const mq = matchMedia('(prefers-color-scheme: dark)');
      const cb = async evt => {
        const { isDark } = await chrome.storage.session.get('isDark');
        if (isDark !== evt.matches) {
          chrome.runtime.sendMessage({
            cmd: 'darkThemeChanged',
            data: evt.matches,
          });
        }
      };
      mq.onchange = cb;
      cb(mq);
    }
    

    You should also include this code block in your extension pages (but not the entire content.js which is meant for web pages) so that the detection occurs when your extension page or popup is opened in a browser that doesn't have any tabs with web pages where you content.js runs.