Differential D301050
Bug 2036647 - Allow extension themes to use CSS gradients. r=#theme!,#extension-reviewers! ClosedPublic Authored by emilio on May 18 2026, 11:54 AM. Tags Referenced Files
Details
Summary And use this for Alpenglow. Note that I had to allow configuring
Diff Detail
Event Timelineemilio created this revision.May 18 2026, 11:54 AM phab-bot published this revision for review.May 18 2026, 11:55 AM 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:
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 Comment Actionsnote: 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;
This revision now requires changes to proceed.May 18 2026, 6:52 PM emilio added a comment.May 18 2026, 10:30 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.
emilio requested review of this revision.Fri, May 29, 5:50 PM emilio updated this revision to Diff 1286979. Comment ActionsRebase 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 Actionslint Harbormaster failed remote builds in B977874: Diff 1287028! dao accepted this revision.Mon, Jun 1, 8:45 AM emilio edited the summary of this revision. (Show Details) Comment Actions re-introduce the tests which were accidentally dropped reviewbot added a comment.Thu, Jun 4, 9:23 AM Comment ActionsCode analysis found 2 defects in diff 1291747:
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 accepted this revision.Thu, Jun 4, 2:08 PM 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 SummaryIntentThe 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. SolutionThe 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.
emilio added inline comments.Thu, Jun 4, 4:58 PM
Closed by commit rFIREFOXAUTOLANDdced783049bc: Bug 2036647 - Allow extension themes to use CSS gradients. r=desktop-theme… (authored by emilio, committed by ealvarez@mozilla.com <ealvarez@mozilla.com>). · Explain WhyThu, Jun 4, 6:18 PM This revision was automatically updated to reflect the committed changes.
Revision Contents
Diff 1292373 toolkit/components/extensions/test/browser/browser_ext_themes_multiple_backgrounds.jsLoading... | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
