Dominik Süß

fighting computers since 1999

in

Simple Firefox themes with dark/light mode support

userChrome too fragile but color.firefox.com too restricted? Why not build your own theme!


I like my environments consistently themed. If an application does not support theming, chances are that I'm not going to use it. Recently, I've also been on a journey to have consistent dark/light mode toggling for all my apps. After switching from foot to alacritty (which I put off for a looooong time to avoid dealing with nixGL), Firefox was the last eyesore remaining.

Up until now, I was very happy with color.firefox.com for my theming needs. I've used a custom userChrome.css before but got tired of updates breaking the style. The only downside is the fact that the color addon does not support dark/light mode. What a shame!

In general, documentation around dynamic dark/light themes is very sparse which is why I'm writing this down.

Creating a custom theme

As previously mentioned, my starting point was the color addon. Taking a closer look at its functionality, an export button piqued my interest. It offers exports in the xpi and zip formats. In the end, both are zip files containing a manifest.json document.

Taking a closer look, this is actually pretty readable:

{
  "manifest_version": 2,
  "version": "1.0",
  "name": "test",
  "theme": {
    "colors": {
      "toolbar": "rgb(43, 206, 227)",
      "toolbar_text": "rgb(215, 226, 239)",
      "frame": "rgb(115, 214, 228)",
      "tab_background_text": "rgb(84, 84, 84)",
      "toolbar_field": "rgba(255, 255, 255, 0.53)",
      "toolbar_field_text": "rgb(255, 71, 203)",
      "tab_line": "rgb(255, 66, 205)",
      "popup": "rgb(231, 242, 244)",
      "popup_text": "rgb(84, 84, 84)",
      "tab_loading": "rgb(255, 66, 205)"
    }
  }
}

On its own, this doesnt get you very far. You now just have a differernt representation of the data you already had in the addon configuration. But one thing that stands out is the way the color codes are specified.

'That's a penis!' meme adapted to say 'That's CSS!'

And a look at the documentation confirms this:

All these properties can be specified as either a string containing any valid CSS color string (including hexadecimal), or an RGB array, such as "tab_background_text": [ 107 , 99 , 23 ].

As luck would have it, CSS supports a way to dynamicaly set a value based on the system preference. light-dark() is a CSS function that returns the first argument for light mode and the second for dark mode.

Using this to write the theme isn't very pretty but it should work:

{
  "manifest_version": 2,
  "version": "1.0",
  "name": "test",
  "theme": {
    "colors": {
      "toolbar": "light-dark(white,black)",
      "toolbar_text": "light-dark(black,white)",
      "frame": "light-dark(red,blue)",
      "tab_background_text": "light-dark(yellow,green)",
      "toolbar_field": "light-dark(white,black)",
      "toolbar_field_text": "light-dark(black,white)",
      "tab_line": "light-dark(blue,pink)",
      "popup": "#aaaaaa",
      "popup_text": "black",
      "tab_loading": "#bada55"
    }
  }
}

To load this, create a zip file containing just this file and load it as a temporary add-on.

Great success - the values for our current scheme are loaded! Let's try switching theme…

Nothing happens? The glorious red of light-mode should switch to blue when the theme is switched so what is happening?

Grepping the documenation for dark or light brings up another property called color_scheme. Setting this to system instead of auto does the trick!

{
  "manifest_version": 2,
  "version": "1.0",
  "name": "test",
  "theme": {
    "properties": {
      "color_scheme": "system"
    },
    "colors": {
      "toolbar": "light-dark(white,black)",
      "toolbar_text": "light-dark(black,white)",
      "frame": "light-dark(red,blue)",
      "tab_background_text": "light-dark(yellow,green)",
      "toolbar_field": "light-dark(white,black)",
      "toolbar_field_text": "light-dark(black,white)",
      "tab_line": "light-dark(blue,pink)",
      "popup": "#aaaaaa",
      "popup_text": "black",
      "tab_loading": "#bada55"
    }
  }
}

It works! With a bit more effort on the color selection, this can even look great!

Keeping the theme

As the name Load Temporary Add-on implies, the zip file you just loaded will be gone on the next Firefox restart. So how can you keep your new theme loaded? By signing it!

Sadly, Firefox does not allow for unsigned or arbitrarily signed plugins to be loaded so you need to go through the plugin signing process of the Firefox marketplace. Don't worry - you won't actually need to make your theme publicly available and manage store listings. The plugin can remain unlisted - just for your enjoyment.

Sign up for an account on the Mozilla Developer Hub and create a new API Key. Next, run the following command to submit your plugin:

npx web-ext sign \
    --api-key=$YOUR_API_KEY \
    --api-secret=$YOUR_API_SECRET \
    -s . --channel=unlisted

This will also present you with a unique plugin ID to add to your manifest later on. The plugin is now submitted to Mozilla and all that's left for you is to wait. For simple themes like this, no human is going to look at your theme and it'll probably be automatically accepted after around 30 minutes.

Once that's done, you can install your new theme through the developer hub and it'll live across restarts 🥳.

Thunderbird bonus round

This being a web extension based plugin should mean that you can just reuse this in thunderbird right?

WRONG! Thunderbird does not know about the system value for color_scheme. But there is a way around this!

While light-dark approach doesn't work, the Thunderbird manifest documentation mentions a top level field called dark_theme. And as you might expect, this allows you to define a separate theme for dark mode.

{
  "manifest_version": 2,
  "version": "1.0",
  "name": "test",
  "theme": {
    "images": {},
    "properties": {
      "color_scheme": "light"
    },
    "colors": {
      "toolbar": "#fff6d8",
      "toolbar_text": "#484431",
      "frame": "#f5e9cb",
      "tab_background_text": "#484431",
      "toolbar_field": "#fff6d8",
      "toolbar_field_text": "#484431",
      "tab_line": "#68708a",
      "popup": "#e7d7c6",
      "popup_text": "#80431a",
      "tab_loading": "#ba2d2f"
    }
  },
  "dark_theme": {
    "images": {},
    "properties": {
      "color_scheme": "dark"
    },
    "colors": {
      "toolbar": "rgb(0, 0, 0)",
      "toolbar_text": "rgb(254, 172, 208)",
      "frame": "rgb(0, 0, 0)",
      "tab_background_text": "rgb(191, 191, 191)",
      "toolbar_field": "rgb(0, 0, 0)",
      "toolbar_field_text": "rgb(191, 191, 191)",
      "tab_line": "rgb(254, 172, 208)",
      "popup": "rgb(0, 0, 0)",
      "popup_text": "rgb(255, 255, 255)",
      "tab_loading": "rgb(254, 172, 208)"
    }
  }
}

Thunderbird also adds some morecolor fields you can customize but to be honest: that's something for future Dominik to do.

Testing works the same way and it turns out security doesn't seem to be as much of a concern in Thunderbird world. Here you can just load the zip file as a regular addon!

It also turns out that Firefox uses dark_theme as well but this is not documented so I'll stick to light-dark for now.

Conclusion

  • use light-dark in Firefox
  • you'll have to sign the plugin for it to stick around
  • use dark_theme in Thunderbird

At some point I'll probably build a small tool to simplify working with this mechanism but for now you can find my themes in this repository. Happy hacking!