VOOZH about

URL: https://phabricator.services.mozilla.com/D301050

⇱ ⚙ D301050 Bug 2036647 - Allow extension themes to use CSS gradients. r=#theme!,#extension-reviewers!


Bug 2036647 - Allow extension themes to use CSS gradients. r=#theme!,#extension-reviewers!
ClosedPublic

Authored by emilio on May 18 2026, 11:54 AM.
Referenced Files
Unknown Object (File)
Mon, Jun 15, 3:09 PM
Unknown Object (File)
Mon, Jun 15, 12:36 PM
Unknown Object (File)
Mon, Jun 15, 12:16 PM
Unknown Object (File)
Mon, Jun 15, 12:16 PM
Unknown Object (File)
Mon, Jun 15, 12:15 PM
Unknown Object (File)
Mon, Jun 15, 12:15 PM
Unknown Object (File)
Mon, Jun 15, 12:15 PM
Unknown Object (File)
Mon, Jun 15, 12:13 PM

Details

Summary

And use this for Alpenglow. Note that I had to allow configuring
background-size too.

Diff Detail

Event Timeline

phab-bot changed the visibility from "Custom Policy" to "Public (No Login Required)".
phab-bot changed the edit policy from "Custom Policy" to "Restricted Project (Project)".
phab-bot removed a project: secure-revision.
Comment Actions

Code analysis found 5 defects in diff 1276815:

  • 1 defect found by stylelint (Mozlint)
  • 4 defects found by eslint (Mozlint)
IMPORTANT: Found 5 defects (error level) that must be fixed before landing.

You can run this analysis locally with:


If you see a problem in this automated review, please report it here.

You can view these defects in the Diff Detail section of Phabricator diff 1276815.

rpl requested changes to this revision.May 18 2026, 6:52 PM
rpl added a subscriber: rpl.
Comment Actions

note: marking as "Request Changes", but it is actually meant to signal that we are still in the process of discussing and get to an agreement about the implementation approach we'd prefer for this patch.

suggestion: @emilio the diff I'm pasting below includes an alternative version of this patch (in other words: it is not a diff of changes not applied on top, it includes a subset of the changes in this patch but with the alternative approach I was mentioning you while we were discussions possible options to achieve this).

note: the patch is meant to be equivalent to what this patch does, but it is using a slightly more explicit JSONSchema type so that the gradient types validation doesn't need to be achieved by changes applied internally in the Schemas.sys.mjs logic and would provide through the existing JSONSchema validation logic the following error message without any change applied to Schemas.sys.mjs:

Extension is invalid

Reading manifest: Error processing theme.images.additional_backgrounds.0: Value must either: be a string value, or .gradient_type must be one of ["linear-gradient", "radial-gradient", "conic-gradient", "repeating-linear-gradient", "repeating-radial-gradient", "repeating-conic-gradient"]

Based on how the new method seems to be currently used in this patch, I'm guessing we may not strictly need that anymore with the alternative approach drafted below (but to be honest I haven't looked closely into what reasons there may still be for it, maybe it may be useful in the future to ignore invalid image data?).

Let me know what do you think about the alternative proposed approach, in particular if you see any reason to still prefer the more implicit approach used in the current version of this phabricator revision.

note: once we have settled on the approach, I'll be more than happy to help to determine what kind of additional test coverage we may want to add to this patch.


diff --git a/browser/themes/BuiltInThemeConfig.sys.mjs b/browser/themes/BuiltInThemeConfig.sys.mjs
index c4ff66417c43..efe786548af7 100644
--- a/browser/themes/BuiltInThemeConfig.sys.mjs
+++ b/browser/themes/BuiltInThemeConfig.sys.mjs
@@ -42,7 +42,7 @@ export const BuiltInThemeConfig = new Map([
 [
 "firefox-alpenglow@mozilla.org",
 {
- version: "1.5.2",
+ version: "1.5.3",
 path: "resource://builtin-themes/alpenglow/",
 },
 ],
diff --git a/browser/themes/ThemeVariableMap.sys.mjs b/browser/themes/ThemeVariableMap.sys.mjs
index e34fe796f0b3..c988f433270a 100644
--- a/browser/themes/ThemeVariableMap.sys.mjs
+++ b/browser/themes/ThemeVariableMap.sys.mjs
@@ -23,6 +23,13 @@ export const ThemeVariableMap = [
 lwtProperty: "backgroundsTiling",
 },
 ],
+ [
+ "--lwt-background-size",
+ {
+ isColor: false,
+ lwtProperty: "backgroundsSize",
+ },
+ ],
 [
 "--tab-loading-fill",
 {
diff --git a/browser/themes/addons/alpenglow/background-gradient-dark.svg b/browser/themes/addons/alpenglow/background-gradient-dark.svg
index 6ab26b42d5e1..e69de29bb2d1 100644
--- a/browser/themes/addons/alpenglow/background-gradient-dark.svg
+++ b/browser/themes/addons/alpenglow/background-gradient-dark.svg
@@ -1,4 +0,0 @@
-<!-- This Source Code Form is subject to the terms of the Mozilla Public
- - License, v. 2.0. If a copy of the MPL was not distributed with this
- - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
-<svg width="72" height="144" xmlns="http://www.w3.org/2000/svg"><defs><linearGradient x1="50%" y1="0%" x2="50%" y2="50%" id="a"><stop stop-color="#20123A" offset="0%"/><stop stop-color="#291D4F" offset="100%"/></linearGradient></defs><path fill="url(#a)" d="M0 0h72v144H0z" fill-rule="evenodd"/></svg>
\ No newline at end of file
diff --git a/browser/themes/addons/alpenglow/background-gradient.svg b/browser/themes/addons/alpenglow/background-gradient.svg
index a0b54a46ad84..e69de29bb2d1 100644
--- a/browser/themes/addons/alpenglow/background-gradient.svg
+++ b/browser/themes/addons/alpenglow/background-gradient.svg
@@ -1,4 +0,0 @@
-<!-- This Source Code Form is subject to the terms of the Mozilla Public
- - License, v. 2.0. If a copy of the MPL was not distributed with this
- - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
-<svg width="72" height="144" xmlns="http://www.w3.org/2000/svg"><defs><linearGradient x1="50%" y1="-18.096%" x2="50%" y2="50%" id="a"><stop stop-color="#FF6BBA" offset="0%"/><stop stop-color="#FFC999" offset="100%"/></linearGradient></defs><path fill="url(#a)" d="M0 0h72v144H0z" fill-rule="evenodd"/></svg>
\ No newline at end of file
diff --git a/browser/themes/addons/alpenglow/manifest.json b/browser/themes/addons/alpenglow/manifest.json
index 648b73a01df3..bddcd24ffb81 100644
--- a/browser/themes/addons/alpenglow/manifest.json
+++ b/browser/themes/addons/alpenglow/manifest.json
@@ -10,7 +10,7 @@
 "name": "Firefox Alpenglow",
 "description": "Use a colorful appearance for buttons, menus, and windows.",
 "author": "Mozilla",
- "version": "1.5.2",
+ "version": "1.5.3",
 "icons": { "32": "icon.svg" },
 
 "theme": {
@@ -18,7 +18,10 @@
 "additional_backgrounds": [
 "background-noodles-right.svg",
 "background-noodles-left.svg",
- "background-gradient.svg"
+ {
+ "gradient_type": "linear-gradient",
+ "gradient_params": ["to bottom", "#FF6BBA -18.096%", "#FFC999 50%"]
+ }
 ]
 },
 
@@ -29,6 +32,7 @@
 "right top"
 ],
 "additional_backgrounds_tiling": ["no-repeat", "no-repeat", "repeat-x"],
+ "additional_backgrounds_size": ["auto", "auto", "auto 144px"],
 "zap_gradient": "linear-gradient(90deg, #9059FF 0%, #FF4AA2 52.08%, #FFBD4F 100%)"
 },
 "colors": {
@@ -74,7 +78,10 @@
 "additional_backgrounds": [
 "background-noodles-right-dark.svg",
 "background-noodles-left-dark.svg",
- "background-gradient-dark.svg"
+ {
+ "gradient_type": "linear-gradient",
+ "gradient_params": ["to bottom", "#20123A 0%", "#291D4F 50%"]
+ }
 ]
 },
 
@@ -85,6 +92,7 @@
 "right top"
 ],
 "additional_backgrounds_tiling": ["no-repeat", "no-repeat", "repeat-x"],
+ "additional_backgrounds_size": ["auto", "auto", "auto 144px"],
 "zap_gradient": "linear-gradient(90deg, #9059FF 0%, #FF4AA2 52.08%, #FFBD4F 100%)"
 },
 "colors": {
diff --git a/browser/themes/shared/browser-shared.css b/browser/themes/shared/browser-shared.css
index 8865cc4a0bb3..a371730e282b 100644
--- a/browser/themes/shared/browser-shared.css
+++ b/browser/themes/shared/browser-shared.css
@@ -116,9 +116,11 @@ body {
 --lwt-additional-images: none;
 --lwt-background-alignment: right top;
 --lwt-background-tiling: no-repeat;
+ --lwt-background-size: auto;
 
 --toolbox-background-image: var(--lwt-additional-images);
 --toolbox-background-repeat: var(--lwt-background-tiling);
+ --toolbox-background-size: var(--lwt-background-size);
 --toolbox-background-position: var(--lwt-background-alignment);
 
 &[lwtheme-image] {
@@ -126,6 +128,7 @@ body {
 the latter atop the former. */
 --toolbox-background-image: var(--lwt-header-image), var(--lwt-additional-images);
 --toolbox-background-repeat: no-repeat, var(--lwt-background-tiling);
+ --toolbox-background-size: auto, var(--lwt-background-size);
 --toolbox-background-position: right top, var(--lwt-background-alignment);
 }
 
@@ -277,6 +280,8 @@ body {
 :root[lwtheme-image-y-align] #navigator-toolbox {
 background-image: var(--toolbox-background-image);
 background-repeat: var(--toolbox-background-repeat);
+ /* stylelint-disable-next-line stylelint-plugin-mozilla/use-design-tokens */
+ background-size: var(--toolbox-background-size);
 background-position: var(--toolbox-background-position);
 }
 
diff --git a/toolkit/components/extensions/schemas/theme.json b/toolkit/components/extensions/schemas/theme.json
index 4cdd70aa1980..912c689d1cf7 100644
--- a/toolkit/components/extensions/schemas/theme.json
+++ b/toolkit/components/extensions/schemas/theme.json
@@ -37,6 +37,29 @@
 }
 ]
 },
+ {
+ "id": "ThemeCSSGradient",
+ "type": "object",
+ "properties": {
+ "gradient_type": {
+ "type": "string",
+ "enum": [
+ "linear-gradient",
+ "radial-gradient",
+ "conic-gradient",
+ "repeating-linear-gradient",
+ "repeating-radial-gradient",
+ "repeating-conic-gradient"
+ ]
+ },
+ "gradient_params": {
+ "choices": [
+ { "type": "string" },
+ { "type": "array", "items": { "type": "string" } }
+ ]
+ }
+ }
+ },
 {
 "id": "ThemeExperiment",
 "type": "object",
@@ -78,7 +101,12 @@
 "properties": {
 "additional_backgrounds": {
 "type": "array",
- "items": { "$ref": "ImageDataOrExtensionURL" },
+ "items": {
+ "choices": [
+ { "$ref": "ImageDataOrExtensionURL" },
+ { "$ref": "ThemeCSSGradient" }
+ ]
+ },
 "maxItems": 15,
 "optional": true
 },
@@ -310,6 +338,14 @@
 "maxItems": 15,
 "optional": true
 },
+ "additional_backgrounds_size": {
+ "type": "array",
+ "items": {
+ "type": "string"
+ },
+ "maxItems": 15,
+ "optional": true
+ },
 "color_scheme": {
 "optional": true,
 "type": "string",
diff --git a/toolkit/modules/LightweightThemeConsumer.sys.mjs b/toolkit/modules/LightweightThemeConsumer.sys.mjs
index 756cf47d4dd5..d2e3c37ee73b 100644
--- a/toolkit/modules/LightweightThemeConsumer.sys.mjs
+++ b/toolkit/modules/LightweightThemeConsumer.sys.mjs
@@ -577,15 +577,31 @@ function _getContentProperties(doc, hasTheme, data) {
 return properties;
 }
 
-function _setImage(aWin, aRoot, aActive, aVariableName, aURLs) {
- if (aURLs && !Array.isArray(aURLs)) {
- aURLs = [aURLs];
+function _imageToCss(aWin, aImage) {
+ if (typeof aImage === "object") {
+ if (aImage.type !== "gradient_css") {
+ console.warn(
+ `LightweightThemeConsumer: unexpected image type "${aImage.type}"`
+ );
+ return null;
+ }
+ return aImage.value;
+ }
+ return `url(${aWin.CSS.escape(aImage)})`;
+}
+
+function _setImage(aWin, aRoot, aActive, aVariableName, aImages) {
+ if (aImages && !Array.isArray(aImages)) {
+ aImages = [aImages];
 }
 _setProperty(
 aRoot,
 aActive,
 aVariableName,
- aURLs && aURLs.map(v => `url(${aWin.CSS.escape(v)})`).join(", ")
+ aImages
+ ?.map(v => _imageToCss(aWin, v))
+ .filter(v => typeof v === "string") // Ignore invalid entries.
+ .join(", ")
 );
 }
 
diff --git a/toolkit/mozapps/extensions/LightweightThemeManager.sys.mjs b/toolkit/mozapps/extensions/LightweightThemeManager.sys.mjs
index 53c0118ab476..6f8061a904ad 100644
--- a/toolkit/mozapps/extensions/LightweightThemeManager.sys.mjs
+++ b/toolkit/mozapps/extensions/LightweightThemeManager.sys.mjs
@@ -17,8 +17,21 @@ function loadImages(images, styles, experiment, baseURI, logger) {
 
 switch (image) {
 case "additional_backgrounds": {
- let backgroundImages = val.map(img => baseURI.resolve(img));
- styles.additionalBackgrounds = backgroundImages;
+ // TODO: consider to also tag URL entries as { type: "image_url", value } for consistency.
+ styles.additionalBackgrounds = val.map(img => {
+ if (typeof img === "object") {
+ let params = Array.isArray(img.gradient_params)
+ ? img.gradient_params.join(",")
+ : img.gradient_params;
+ return {
+ type: "gradient_css",
+ value: `${img.gradient_type}(${params})`,
+ };
+ }
+ return baseURI.resolve(img);
+ });
 break;
 }
 case "theme_frame": {
@@ -168,6 +181,18 @@ function loadProperties(properties, styles, experiment, logger) {
 styles.backgroundsTiling = tiling.join(",");
 break;
 }
+ case "additional_backgrounds_size": {
+ if (!assertValidAdditionalBackgrounds(property, val.length)) {
+ break;
+ }
+
+ let sizes = [];
+ for (let i = 0, l = styles.additionalBackgrounds.length; i < l; ++i) {
+ sizes.push(val[i] || "auto");
+ }
+ styles.backgroundsSize = sizes.join(",");
+ break;
+ }
 case "color_scheme":
 case "content_color_scheme": {
 styles[property] = val;
browser/themes/addons/alpenglow/manifest.json
90

question: @emilio I noticed this zap_gradient property while looking to this patch, I have to admit that I never noticed it before today, do you know any more details about the history behind this zap_gradient property that I may have missed? Also that got me wonder: could what this zap_gradient does be replaced by an additional gradient entry to the additional_backgrounds array with the gradient support we are planning to add as part of this change?

toolkit/components/extensions/Schemas.sys.mjs
37–46

thought: the fact that we are adding custom validation logic here is someone I was expecting to happen by trying to fit the new gradients into the same field where we currently expect either a relative path to an asset packaged in the extension (or image data if the theme property is coming from the theme API instead of the manifest.json file), the alternative path I was briefly mentioning in the meeting we had last Friday is to tweak the JSON schema to:

  • (1) being able to tell a gradient background type apart from a relative url to an image asset packaged in the extension right away based on the schema validation, so that AMO can also do the same and not need to detect from the string value if the entry is expected to be an image url or a gradient.
  • (2) inherit a similar validation on the addons-linter side through the regular JSONSchema data import (from Firefox into the addons-linter repository) that we are already doing periodically on each new Firefox beta version, so that we can also make sure we will apply a similar preliminary validation on the addons-linter side, which runs at submittion time and would be done before the "AMO logic handling the new theme manifest.json and creating a new theme preview image" would be hit

I think that may also be enough to not need the addition to introduce a new InspectorUtils.isValidCSSImage (unless there are other reasons for adding that helper).

This revision now requires changes to proceed.May 18 2026, 6:52 PM
Comment Actions

@rpl my main concern with making it more explicit / using an object, is that discourages usage, and we really prefer CSS gradients to SVG images... See my comment in the bug.

browser/themes/addons/alpenglow/manifest.json
90

question: @emilio I noticed this zap_gradient property while looking to this patch, I have to admit that I never noticed it before today, do you know any more details about the history behind this zap_gradient property that I may have missed? Also that got me wonder: could what this zap_gradient does be replaced by an additional gradient entry to the additional_backgrounds array with the gradient support we are planning to add as part of this change?

No, is the gradient for the app menu, so totally tangential to this. Note that it is used in the experiment section of the theme so it's not exposed to arbitrary extensions...

emilio updated this revision to Diff 1286979.
Comment Actions

Rebase

emilio updated this revision to Diff 1287030.
emilio edited the summary of this revision. (Show Details)
emilio updated this revision to Diff 1287034.
emilio updated this revision to Diff 1287036.
Comment Actions

lint

jwatt added a subscriber: jwatt.
Comment Actions

r+ for layout and style.

emilio edited the summary of this revision. (Show Details)
Comment Actions

re-introduce the tests which were accidentally dropped

Comment Actions

Code analysis found 2 defects in diff 1291747:

  • 2 defects found by stylelint (Mozlint)
IMPORTANT: Found 2 defects (error level) that must be fixed before landing.

You can run this analysis locally with:


If you see a problem in this automated review, please report it here.

You can view these defects in the Diff Detail section of Phabricator diff 1291747.

willdurand added a project: testing-approved.
willdurand removed a reviewer: rpl.
This revision is now accepted and ready to land.Thu, Jun 4, 2:08 PM
Comment Actions

(the patch summary is outdated)

Comment Actions

Summary

Intent

The goal of these changes is to allow Firefox extension themes to specify CSS gradients (such as , , , and their repeating variants) directly as background images in theme manifests, instead of requiring gradient effects to be wrapped in SVG image files. Additionally, the changes introduce support for a new property so theme authors can control the of each background layer. The Alpenglow built-in theme is updated to take advantage of this new capability by replacing its SVG gradient assets with inline CSS gradient declarations.

Solution

The implementation spans several layers of the codebase:

Schema and validation: A new type is defined in the theme JSON schema, allowing theme images (both and ) to accept an object whose single property name is a CSS gradient function (e.g., ) and whose value is the gradient's arguments string. A postprocessor is added to that validates the constructed gradient string using the new method. For invalid gradients, programmatic theme API calls throw an error, while static manifest themes log a warning and fall back to .

CSS image validation plumbing: A new function is exposed from the Servo/Gecko style engine (in Rust), which parses a string as a CSS value. This is surfaced through , , and the interface so it can be called from JavaScript.

Theme data processing: In , a helper distinguishes between string URLs (which get resolved against a base URI) and gradient objects (which are passed through as-is). The internal property name for the main theme frame image is renamed from to to reflect that it can now be either a URL or a gradient object. A new property is processed alongside the existing tiling property.

Theme consumption: In , a new helper converts image values to CSS: gradient objects are rendered as strings, while URL strings continue to use syntax. References to the old property are updated to .

CSS and theme variables: A new CSS custom property is added to the theme variable map and wired through to control on the toolbox, alongside the existing tiling and alignment properties. Default values are adjusted for consistency.

Alpenglow update: The Alpenglow theme manifest replaces its SVG gradient background files ( and ) with inline objects and adds entries. The SVG files are deleted, and the theme version is bumped. The AI Window theme's CSS is also updated to use the new variable instead of directly setting .

Tests: New test cases verify that gradients are correctly applied as background-image layers interleaved with regular images, that and tiling are respected for gradient layers, and that invalid gradient arguments are properly rejected with appropriate error messages for both static themes and the programmatic theme API.


Please use / reactions on inline comments to provide feedback. This will have a significant impact on the quality of future reviews.
toolkit/components/extensions/schemas/theme.json
365

items accept any string without validation, unlike (enum of repeat values) and (enum of position keywords). Since the value is substituted into via a custom property, a single invalid item (e.g. a typo) would make the *entire* declaration invalid at computed-value time, silently breaking sizing for all background layers. Add validation — either a pattern/format constraint covering common values (, , , ), or use a CSS parser check similar to the gradient validation.

toolkit/components/extensions/schemas/theme.json
365

items accept any string without validation, unlike (enum of repeat values) and (enum of position keywords). Since the value is substituted into via a custom property, a single invalid item (e.g. a typo) would make the *entire* declaration invalid at computed-value time, silently breaking sizing for all background layers. Add validation — either a pattern/format constraint covering common values (, , , ), or use a CSS parser check similar to the gradient validation.

Background-size is not security sensitive, unlike background-image which can trigger loads. Adding some sort of background-size validator seems worth maybe but might be done as a follow-up if we see this tripping up people? It's useful to be able to specify things like or so...

emilio edited the summary of this revision. (Show Details)
Comment Actions

lint + commit message fix

Revision Contents

PathSize
browser/
themes/
2 lines
7 lines
addons/
aiwindow/
4 lines
alpenglow/
8 lines
shared/
11 lines
dom/
chrome-webidl/
1 line
layout/
inspector/
4 lines
6 lines
style/
6 lines
5 lines
servo/
ports/
geckolib/
20 lines
toolkit/
components/
extensions/
19 lines
schemas/
61 lines
test/
browser/
176 lines
modules/
31 lines
mozapps/
extensions/
27 lines
test/
browser/
4 lines
CommitTreeParentsAuthorSummaryDate
dced783049bc3b300d382b80Emilio Cobos Álvarez
Bug 2036647 - Allow extension themes to use CSS gradients. r=desktop-theme… (Show More…)

Diff 1292373

browser/themes/BuiltInThemeConfig.sys.mjs

Loading...

browser/themes/ThemeVariableMap.sys.mjs

Loading...

browser/themes/addons/aiwindow/aiwindow-theme.css

Loading...

browser/themes/addons/alpenglow/background-gradient-dark.svg

Loading...

browser/themes/addons/alpenglow/background-gradient.svg

Loading...

browser/themes/addons/alpenglow/manifest.json

Loading...

browser/themes/shared/browser-shared.css

Loading...

dom/chrome-webidl/InspectorUtils.webidl

Loading...

layout/inspector/InspectorUtils.h

Loading...

layout/inspector/InspectorUtils.cpp

Loading...

layout/style/ServoCSSParser.h

Loading...

layout/style/ServoCSSParser.cpp

Loading...

servo/ports/geckolib/glue.rs

Loading...

toolkit/components/extensions/Schemas.sys.mjs

Loading...

toolkit/components/extensions/schemas/theme.json

Loading...

toolkit/components/extensions/test/browser/browser_ext_themes_multiple_backgrounds.js

Loading...

toolkit/modules/LightweightThemeConsumer.sys.mjs

Loading...

toolkit/mozapps/extensions/LightweightThemeManager.sys.mjs

Loading...

toolkit/mozapps/extensions/test/browser/browser_webapi_theme.js

Loading...