Monday, 4 December, 2017 UTC


Summary

From powerful extensions like Stratiform or FT Deep Dark to simple lightweight themes, theming has been quite popular within Firefox. Now that Firefox Quantum (57) has launched with many performance improvements and a sparkling new interface, we want to bridge the gap with a new theming API that allows you to go beyond basic lightweight themes.
Demo by John Gruen
What can you theme?
Before the launch of Quantum, lightweight themes had a limited set of properties that could be themed: you could only add a header image and set the frame text color and background color. The new theming API introduces some new properties. The full list can be found on MDN. A basic Theme object looks like this:
{
  "colors": {
    "accentcolor": "tomato",
    "textcolor": "white",
    "toolbar": "#444",
    "toolbar_text": "lightgray",
    "toolbar_field": "black",
    "toolbar_field_text": "white"
  },
  "images": {
    "headerURL": ""
  }
}
Here’s how the above theme is displayed:
Notice how the images.headerURL property is set to an empty string. This is because it is one of three mandatory properties: images.headerURL, colors.accentcolor and colors.textcolor.
Finally, another improvement to lightweight themes is support for multiple header images, using the images.additional_backgrounds field which takes an array of image paths. The alignments and tilings of these images is achieved using properties.additional_backgrounds_alignment and properties.additional_backgrounds_tiling, which take in an array of background-position and background-repeat values respectively. You can check out the MDN page for an example. You can use multiple backgrounds in order to display curtains on both sides of the browser UI, or as a way to add several thematic indicators (sports/weather/private browsing) in the UI.
Dynamic themes
Let’s say you would like to introduce a night mode to your theme. Dynamic themes allow you to do this. They have the full power of a normal browser extension. To use dynamic theming, you need to add the theme permission to your manifest.
The browser.theme.update() method is at the core of this type of theming. It takes in a Theme object as parameter. The method can be called anywhere in your background scripts.
For this example, let’s create an extension that switches the theme depending on whether it’s night or day. The first step is to create a function in your background script that switches your theme to the day theme or the night theme:
var currentTheme = '';

const themes = {
  'day': {
    images: {
      headerURL: 'sun.jpg',
    },
    colors: {
      accentcolor: '#CF723F',
      textcolor: '#111',
    }
  },
  'night': {
    images: {
      headerURL: 'moon.jpg',
    },
    colors: {
      accentcolor: '#000',
      textcolor: '#fff',
    }
  }
};

function setTheme(theme) {
  if (currentTheme === theme) {
    // No point in changing the theme if it has already been set.
    return;
  }
  currentTheme = theme;
  browser.theme.update(themes[theme]);
}
The above code defines two themes: the day theme and the night theme, the setTheme function then uses browser.theme.update() to set the theme.
The next step is now to use this setTheme function and periodically check whether the extension should switch themes. You can do this using the alarms API. The code below checks periodically and sets the theme accordingly:
function checkTime() {
  let date = new Date();
  let hours = date.getHours();
  // Will set the sun theme between 8am and 8pm.
  if (hours > 8 && hours < 20) {
    setTheme('day');
  } else {
    setTheme('night');
  }
}

// On start up, check the time to see what theme to show.
checkTime();

// Set up an alarm to check this regularly.
browser.alarms.onAlarm.addListener(checkTime);
browser.alarms.create('checkTime', {periodInMinutes: 5});
That’s it for this example! The full example is available on the webextension-examples github repository.
Another method that’s not covered by the example is browser.theme.reset(). This method simply resets the theme to the default browser theme.
Per-window themes
The dynamic theming API is pretty powerful, but what if you need to apply a different theme for private windows or inactive windows? From Firefox 57 onwards, it is possible to specify a windowId parameter to both browser.theme.update() and browser.theme.reset(). The windowId is the same ID returned by the windows API.
Let’s make a simple example that adds a dark theme to private windows and keeps other windows set to the default theme:
We start by defining the themeWindow function:
function themeWindow(window) {
  // Check if the window is in private browsing
  if (window.incognito) {
    browser.theme.update(window.id, {
      images: {
        headerURL: "",
      },
      colors: {
        accentcolor: "black",
        textcolor: "white",
        toolbar: "#333",
        toolbar_text: "white"
      }
    });
  }
  // Reset to the default theme otherwise
  else {
    browser.theme.reset(window.id);
  }
}
Once that’s done, we can wire this up with the windows API:
browser.windows.onCreated.addListener(themeWindow);

// Theme all currently open windows
browser.windows.getAll().then(wins => wins.forEach(themeWindow));
Pretty straightforward right? The full example can be found here. Here is how the example looks:
Another add-on that makes use of these capabilities is the Containers theme by Jonathan Kingston, which sets the theme of each window to the container of its selected tab. The source code for this add-on can be found here.
The VivaldiFox add-on also makes use of this capability to display different website themes across different windows:
Obtaining information about the current theme
From Firefox 58 onward, you can now obtain information about the current theme and watch for theme updates. Here’s why this matters:
This allows add-ons to integrate their user interface seamlessly with the user’s currently installed theme. An example of this would be matching your sidebar tabs colors with the colors from your current theme.
To do so, Firefox 58 provides two new APIs: browser.theme.getCurrent() and browser.theme.onUpdated.
Here is a simple example that applies some of the current theme properties to the style of a sidebar_action:
function setSidebarStyle(theme) {
  const myElement = document.getElementById("myElement");

  // colors.frame and colors.accentcolor are aliases
  if (theme.colors && (theme.colors.accentcolor || theme.colors.frame)) {
    document.body.style.backgroundColor =
      theme.colors.accentcolor || theme.colors.frame;
  } else {
    document.body.style.backgroundColor = "white";
  }

  if (theme.colors && theme.colors.toolbar) {
    myElement.style.backgroundColor = theme.colors.toolbar;
  } else {
    myElement.style.backgroundColor = "#ebebeb";
  }
  
  if (theme.colors && theme.colors.toolbar_text) {
    myElement.style.color = theme.colors.toolbar_text;
  } else {
    myElement.style.color = "black";
  }
}

// Set the element style when the extension page loads
browser.theme.getCurrent().then(setSidebarStyle);

// Watch for theme updates
browser.theme.onUpdated.addListener(async ({ theme, windowId }) => {
  const sidebarWindow = await browser.windows.getCurrent();
  /*
    Only update theme if it applies to the window the sidebar is in.
    If a windowId is passed during an update, it means that the theme is applied to that specific window.
    Otherwise, the theme is applied globally to all windows.
  */
  if (!windowId || windowId == sidebarWindow.id) {
    setSidebarStyle(theme);
  }
});
The full example can be found on Github. As you can see in the screenshot below, the sidebar uses colors from the currently applied browser theme:
Another example is the Tree Style Tab add-on which makes use of these APIs to integrate its interface with the currently used theme. Here is a screencast of the add-on working together with VivaldiFox:
What’s next?
There is more coming to this API! We plan to expand the set of supported properties and polish some rough edges around the way themes are applied. The tracking bug for the API can be found on Bugzilla.
In the meanwhile, we can’t wait to see what you will be able to do with the new theming API. Please let us know what improvements you would like to see.