commit d754e748bbe1327276fcd42a09d630067303907f
parent e7d971e5ce8dde935f0e19a761747c652c998286
Author: dustin-jw <dustin@heysparkbox.com>
Date: Thu, 16 Oct 2025 16:19:02 +0000
Bug 1988856 - Add a Stylelint rule to enforce using background-color tokens r=frontend-codestyle-reviewers,hjones
Differential Revision: https://phabricator.services.mozilla.com/D267619
Diffstat:
8 files changed, 1086 insertions(+), 0 deletions(-)
diff --git a/.stylelintrc.js b/.stylelintrc.js
@@ -275,6 +275,7 @@ module.exports = {
"stylelint-plugin-mozilla/use-font-size-tokens": true,
"stylelint-plugin-mozilla/use-font-weight-tokens": true,
"stylelint-plugin-mozilla/use-space-tokens": true,
+ "stylelint-plugin-mozilla/use-background-color-tokens": true,
},
overrides: [
@@ -420,6 +421,7 @@ module.exports = {
"stylelint-plugin-mozilla/use-font-size-tokens": false,
"stylelint-plugin-mozilla/use-font-weight-tokens": false,
"stylelint-plugin-mozilla/use-space-tokens": false,
+ "stylelint-plugin-mozilla/use-background-color-tokens": false,
},
},
{
@@ -432,6 +434,7 @@ module.exports = {
"stylelint-plugin-mozilla/use-border-radius-tokens": true,
"stylelint-plugin-mozilla/use-border-color-tokens": false,
"stylelint-plugin-mozilla/use-space-tokens": false,
+ "stylelint-plugin-mozilla/use-background-color-tokens": false,
},
},
],
diff --git a/docs/code-quality/lint/linters/stylelint-plugin-mozilla/rules/use-background-color-tokens.rst b/docs/code-quality/lint/linters/stylelint-plugin-mozilla/rules/use-background-color-tokens.rst
@@ -0,0 +1,117 @@
+===========================
+use-background-color-tokens
+===========================
+
+This rule checks that CSS declarations use background-color design token variables
+instead of hard-coded values. This ensures consistent background-color across
+the application and makes it easier to maintain design system adoption.
+
+Examples of incorrect code for this rule:
+-----------------------------------------
+
+.. code-block:: css
+
+ .card {
+ background-color: #191919;
+ }
+
+.. code-block:: css
+
+ .custom-button {
+ background: url('image.png') rgba(42 42 42 / 0.15);
+ }
+
+.. code-block:: css
+
+ button:hover {
+ background: rgba(0 0 0 / 0.25);
+ }
+
+.. code-block:: css
+
+ :root {
+ --my-token: blue;
+ }
+
+ .my-button {
+ background: url('image.png') no-repeat center center / auto var(--my-token, oklch(55% 0.21 15));
+ }
+
+.. code-block:: css
+
+ .accent-background-color {
+ background-color: AccentColor;
+ }
+
+Examples of correct token usage for this rule:
+----------------------------------------------
+
+.. code-block:: css
+
+ .card {
+ background-color: var(--background-color-box);
+ }
+
+.. code-block:: css
+
+ .custom-button {
+ background: url('image.png') var(--background-color-box);
+ }
+
+.. code-block:: css
+
+ button:hover {
+ background: var(--background-color-box);
+ }
+
+.. code-block:: css
+
+ /* You may set a fallback for a token. */
+
+ .my-button {
+ background: var(--background-color-box, oklch(55% 0.21 15));
+ }
+
+.. code-block:: css
+
+ /* Local CSS variables that reference valid border-radius tokens are allowed */
+
+ :root {
+ --my-token: var(--background-color-box);
+ }
+
+ .my-button {
+ background-color: var(--my-token, oklch(55% 0.21 15));
+ }
+
+The rule also allows these non-token values:
+
+.. code-block:: css
+
+ .transparent-background-color {
+ background-color: transparent;
+ }
+
+.. code-block:: css
+
+ .inherited-background-color{
+ background-color: inherit;
+ }
+
+.. code-block:: css
+
+ .unset-background-color {
+ background-color: unset;
+ }
+
+.. code-block:: css
+
+ .initial-background-color {
+ background-color: initial;
+ }
+
+.. code-block:: css
+
+ .current-background-color {
+ background-color: currentColor;
+ }
diff --git a/stylelint-rollouts.config.js b/stylelint-rollouts.config.js
@@ -932,4 +932,226 @@ module.exports = [
"tools/tryselect/selectors/chooser/static/style.css",
],
},
+ {
+ // stylelint fixes for this rule will be addressed in Bug 1993105
+ name: "rollout-use-background-color-tokens",
+ rules: {
+ "stylelint-plugin-mozilla/use-background-color-tokens": null,
+ },
+ files: [
+ "browser/branding/aurora/content/aboutDialog.css",
+ "browser/branding/aurora/stubinstaller/installing_page.css",
+ "browser/branding/nightly/content/aboutDialog.css",
+ "browser/branding/nightly/stubinstaller/installing_page.css",
+ "browser/branding/official/content/aboutDialog.css",
+ "browser/branding/official/stubinstaller/installing_page.css",
+ "browser/branding/official/stubinstaller/profile_cleanup_page.css",
+ "browser/branding/unofficial/content/aboutDialog.css",
+ "browser/branding/unofficial/stubinstaller/installing_page.css",
+ "browser/components/aboutlogins/content/aboutLogins.css",
+ "browser/components/aboutlogins/content/components/confirmation-dialog.css",
+ "browser/components/aboutlogins/content/components/generic-dialog.css",
+ "browser/components/aboutlogins/content/components/login-alert.css",
+ "browser/components/aboutlogins/content/components/login-item.css",
+ "browser/components/aboutlogins/content/components/login-timeline.css",
+ "browser/components/aboutlogins/content/components/remove-logins-dialog.css",
+ "browser/components/aboutwelcome/content-src/aboutwelcome.scss",
+ "browser/components/asrouter/content-src/components/ASRouterAdmin/ASRouterAdmin.scss",
+ "browser/components/asrouter/content-src/styles/_feature-callout.scss",
+ "browser/components/contextualidentity/content/usercontext.css",
+ "browser/components/enterprisepolicies/content/aboutPolicies.css",
+ "browser/components/firefoxview/card-container.css",
+ "browser/components/firefoxview/firefoxview.css",
+ "browser/components/firefoxview/fxview-tab-row.css",
+ "browser/components/firefoxview/opentabs-tab-row.css",
+ "browser/components/genai/chat.css",
+ "browser/components/genai/content/link-preview-card.css",
+ "browser/components/ipprotection/content/ipprotection-content.css",
+ "browser/components/preferences/widgets/nav-notice/nav-notice.css",
+ "browser/components/profiles/content/avatar.css",
+ "browser/components/profiles/content/profile-avatar-selector.css",
+ "browser/components/profiles/content/profiles-theme-card.css",
+ "browser/components/protections/content/protections.css",
+ "browser/components/screenshots/overlay/overlay.css",
+ "browser/components/screenshots/screenshots-buttons.css",
+ "browser/components/search/content/contentSearchUI.css",
+ "browser/components/search/test/browser/telemetry/serp.css",
+ "browser/components/sidebar/sidebar-customize.css",
+ "browser/components/sidebar/sidebar-main.css",
+ "browser/components/sidebar/sidebar-pins-promo.css",
+ "browser/components/sidebar/sidebar.css",
+ "browser/components/textrecognition/textrecognition.css",
+ "browser/components/urlbar/tests/browser/dynamicResult0.css",
+ "browser/components/urlbar/tests/browser/dynamicResult1.css",
+ "browser/extensions/newtab/content-src/components/Base/_Base.scss",
+ "browser/extensions/newtab/content-src/components/Card/_Card.scss",
+ "browser/extensions/newtab/content-src/components/CollapsibleSection/_CollapsibleSection.scss",
+ "browser/extensions/newtab/content-src/components/ConfirmDialog/_ConfirmDialog.scss",
+ "browser/extensions/newtab/content-src/components/ContextMenu/_ContextMenu.scss",
+ "browser/extensions/newtab/content-src/components/CustomizeMenu/_CustomizeMenu.scss",
+ "browser/extensions/newtab/content-src/components/DiscoveryStreamAdmin/DiscoveryStreamAdmin.scss",
+ "browser/extensions/newtab/content-src/components/DiscoveryStreamComponents/AdBanner/_AdBanner.scss",
+ "browser/extensions/newtab/content-src/components/DiscoveryStreamComponents/CardGrid/_CardGrid.scss",
+ "browser/extensions/newtab/content-src/components/DiscoveryStreamComponents/DSCard/_DSCard.scss",
+ "browser/extensions/newtab/content-src/components/DiscoveryStreamComponents/DSDismiss/_DSDismiss.scss",
+ "browser/extensions/newtab/content-src/components/DiscoveryStreamComponents/DSEmptyState/_DSEmptyState.scss",
+ "browser/extensions/newtab/content-src/components/DiscoveryStreamComponents/DSImage/_DSImage.scss",
+ "browser/extensions/newtab/content-src/components/DiscoveryStreamComponents/DSSignup/DSSignup.scss",
+ "browser/extensions/newtab/content-src/components/DiscoveryStreamComponents/DSTextPromo/_DSTextPromo.scss",
+ "browser/extensions/newtab/content-src/components/DiscoveryStreamComponents/FeatureHighlight/_FeatureHighlight.scss",
+ "browser/extensions/newtab/content-src/components/DiscoveryStreamComponents/Highlights/_Highlights.scss",
+ "browser/extensions/newtab/content-src/components/DiscoveryStreamComponents/InterestPicker/_InterestPicker.scss",
+ "browser/extensions/newtab/content-src/components/DiscoveryStreamComponents/ListFeed/_ListFeed.scss",
+ "browser/extensions/newtab/content-src/components/DiscoveryStreamComponents/PromoCard/_PromoCard.scss",
+ "browser/extensions/newtab/content-src/components/DiscoveryStreamComponents/TopicSelection/_TopicSelection.scss",
+ "browser/extensions/newtab/content-src/components/DiscoveryStreamComponents/TopicsWidget/_TopicsWidget.scss",
+ "browser/extensions/newtab/content-src/components/DiscoveryStreamComponents/TrendingSearches/_TrendingSearches.scss",
+ "browser/extensions/newtab/content-src/components/DownloadModalToggle/_DownloadModalToggle.scss",
+ "browser/extensions/newtab/content-src/components/ModalOverlay/_ModalOverlay.scss",
+ "browser/extensions/newtab/content-src/components/Search/_Search.scss",
+ "browser/extensions/newtab/content-src/components/TopSites/_TopSites.scss",
+ "browser/extensions/newtab/content-src/components/WallpaperCategories/_WallpaperCategories.scss",
+ "browser/extensions/newtab/content-src/components/Weather/_Weather.scss",
+ "browser/extensions/newtab/content-src/components/Widgets/FocusTimer/_FocusTimer.scss",
+ "browser/extensions/newtab/content-src/components/Widgets/Lists/_Lists.scss",
+ "browser/extensions/newtab/content-src/styles/_mixins.scss",
+ "browser/extensions/newtab/content-src/styles/_variables.scss",
+ "browser/extensions/newtab/content-src/styles/activity-stream.scss",
+ "browser/extensions/webcompat/about-compat/aboutCompat.css",
+ "browser/themes/linux/browser.css",
+ "browser/themes/linux/places/organizer.css",
+ "browser/themes/osx/browser.css",
+ "browser/themes/osx/downloads/allDownloadsView.css",
+ "browser/themes/shared/UITour.css",
+ "browser/themes/shared/addons/unified-extensions.css",
+ "browser/themes/shared/autocomplete.css",
+ "browser/themes/shared/blockedSite.css",
+ "browser/themes/shared/browser-shared.css",
+ "browser/themes/shared/controlcenter/panel.css",
+ "browser/themes/shared/customizableui/customizeMode.css",
+ "browser/themes/shared/customizableui/panelUI-shared.css",
+ "browser/themes/shared/downloads/allDownloadsView.inc.css",
+ "browser/themes/shared/downloads/downloads.inc.css",
+ "browser/themes/shared/downloads/progressmeter.css",
+ "browser/themes/shared/formautofill-notification.css",
+ "browser/themes/shared/identity-block/identity-block.css",
+ "browser/themes/shared/identity-credential-notification.css",
+ "browser/themes/shared/notification-icons.css",
+ "browser/themes/shared/pageInfo.css",
+ "browser/themes/shared/places/editBookmark.css",
+ "browser/themes/shared/places/sidebar.css",
+ "browser/themes/shared/preferences/preferences.css",
+ "browser/themes/shared/preferences/privacy.css",
+ "browser/themes/shared/preferences/search.css",
+ "browser/themes/shared/privatebrowsing/aboutPrivateBrowsing.css",
+ "browser/themes/shared/search/searchbar.css",
+ "browser/themes/shared/sidebar.css",
+ "browser/themes/shared/syncedtabs/sidebar.css",
+ "browser/themes/shared/tabbrowser/content-area.css",
+ "browser/themes/shared/tabbrowser/ctrlTab.css",
+ "browser/themes/shared/tabbrowser/fullscreen-and-pointerlock.css",
+ "browser/themes/shared/tabbrowser/tabs.css",
+ "browser/themes/shared/toolbarbutton-icons.css",
+ "browser/themes/shared/toolbarbuttons.css",
+ "browser/themes/shared/translations/panel.css",
+ "browser/themes/shared/urlbar-dynamic-results.css",
+ "browser/themes/shared/urlbar-searchbar.css",
+ "browser/themes/shared/urlbarView.css",
+ "browser/themes/shared/webRTC-indicator.css",
+ "browser/themes/windows/browser.css",
+ "browser/themes/windows/downloads/allDownloadsView.css",
+ "browser/themes/windows/downloads/downloads.css",
+ "browser/themes/windows/places/organizer.css",
+ "browser/tools/mozscreenshots/mozscreenshots/extension/lib/mozscreenshots-style.css",
+ "dom/base/test/file_bug498897.css",
+ "dom/crypto/test/test_WebCrypto.css",
+ "dom/events/test/pointerevents/wpt/pointerevent_styles.css",
+ "dom/security/test/csp/file_docwrite_meta.css",
+ "dom/xml/resources/XMLPrettyPrint.css",
+ "dom/xml/test/old/books/classic.css",
+ "gfx/layers/layerviewer/tree.css",
+ "layout/generic/test/frame_selection_underline.css",
+ "layout/inspector/tests/bug1202095.css",
+ "layout/mathml/mathml.css",
+ "layout/style/TopLevelImageDocument.css",
+ "layout/style/TopLevelVideoDocument.css",
+ "layout/style/res/forms.css",
+ "layout/style/res/html.css",
+ "layout/style/res/ua.css",
+ "layout/style/res/viewsource.css",
+ "layout/style/test/file_shared_sheet_caching.css",
+ "remote/marionette/reftest-chrome/reftest.css",
+ "security/manager/ssl/tests/mochitest/mixedcontent/somestyle.css",
+ "testing/mochitest/static/harness.css",
+ "testing/mochitest/tests/SimpleTest/test.css",
+ "testing/mozbase/mozlog/mozlog/formatters/html/style.css",
+ "testing/talos/talos/tests/scroll/reader.css",
+ "toolkit/components/aboutconfig/content/aboutconfig.css",
+ "toolkit/components/aboutinference/content/aboutInference.css",
+ "toolkit/components/aboutinference/content/model-files-view.css",
+ "toolkit/components/aboutprocesses/content/aboutProcesses.css",
+ "toolkit/components/aboutwebauthn/content/aboutWebauthn.css",
+ "toolkit/components/certviewer/content/components/info-item.css",
+ "toolkit/components/certviewer/content/components/list-item.css",
+ "toolkit/components/cleardata/tests/browser/file_css_cache.css",
+ "toolkit/components/normandy/content/about-studies/about-studies.css",
+ "toolkit/components/printing/content/printPagination.css",
+ "toolkit/components/resistfingerprinting/content/letterboxing.css",
+ "toolkit/components/satchel/megalist/content/components/login-form/login-form.css",
+ "toolkit/components/satchel/megalist/content/components/password-card/password-card.css",
+ "toolkit/components/satchel/megalist/content/megalist.css",
+ "toolkit/content/aboutLogging/aboutLogging.css",
+ "toolkit/content/aboutMozilla.css",
+ "toolkit/content/aboutTelemetry.css",
+ "toolkit/content/aboutwebrtc/aboutWebrtc.css",
+ "toolkit/content/tests/widgets/videomask.css",
+ "toolkit/content/widgets/infobar.css",
+ "toolkit/content/widgets/moz-button/moz-button.css",
+ "toolkit/content/widgets/moz-input-text/moz-input-text.css",
+ "toolkit/content/widgets/moz-page-nav/moz-page-nav-button.css",
+ "toolkit/content/widgets/moz-page-nav/moz-page-nav.css",
+ "toolkit/content/widgets/moz-reorderable-list/moz-reorderable-list.css",
+ "toolkit/content/widgets/moz-toggle/moz-toggle.css",
+ "toolkit/content/widgets/panel-list/panel-item.css",
+ "toolkit/content/widgets/panel-list/panel-list.css",
+ "toolkit/content/xul.css",
+ "toolkit/mozapps/extensions/content/aboutaddons.css",
+ "toolkit/mozapps/extensions/content/shortcuts.css",
+ "toolkit/themes/linux/global/autocomplete.css",
+ "toolkit/themes/linux/global/richlistbox.css",
+ "toolkit/themes/linux/mozapps/update/updates.css",
+ "toolkit/themes/mobile/global/aboutMemory.css",
+ "toolkit/themes/mobile/global/aboutSupport.css",
+ "toolkit/themes/osx/global/richlistbox.css",
+ "toolkit/themes/shared/aboutCache.css",
+ "toolkit/themes/shared/aboutNetError.css",
+ "toolkit/themes/shared/aboutReader.css",
+ "toolkit/themes/shared/aboutSupport.css",
+ "toolkit/themes/shared/alert.css",
+ "toolkit/themes/shared/arrowscrollbox.css",
+ "toolkit/themes/shared/close-icon.css",
+ "toolkit/themes/shared/datetimeinputpickers.css",
+ "toolkit/themes/shared/design-system/tokens-table.css",
+ "toolkit/themes/shared/dirListing/dirListing.css",
+ "toolkit/themes/shared/findbar.css",
+ "toolkit/themes/shared/global-shared.css",
+ "toolkit/themes/shared/in-content/common-shared.css",
+ "toolkit/themes/shared/in-content/info-pages.css",
+ "toolkit/themes/shared/menu.css",
+ "toolkit/themes/shared/narrate.css",
+ "toolkit/themes/shared/pictureinpicture/player.css",
+ "toolkit/themes/shared/pictureinpicture/texttracks.css",
+ "toolkit/themes/shared/popup.css",
+ "toolkit/themes/shared/splitter.css",
+ "toolkit/themes/shared/tabbox.css",
+ "toolkit/themes/shared/toolbarbutton.css",
+ "toolkit/themes/shared/tree/tree.css",
+ "toolkit/themes/windows/global/autocomplete.css",
+ "toolkit/themes/windows/global/button.css",
+ "toolkit/themes/windows/global/richlistbox.css",
+ "toolkit/themes/windows/global/wizard.css",
+ "toolkit/themes/windows/mozapps/update/updates.css",
+ "tools/tryselect/selectors/chooser/static/style.css",
+ ],
+ },
];
diff --git a/tools/lint/stylelint/stylelint-plugin-mozilla/helpers.mjs b/tools/lint/stylelint/stylelint-plugin-mozilla/helpers.mjs
@@ -6,6 +6,12 @@
import valueParser from "postcss-value-parser";
import { tokensTable } from "../../../../toolkit/themes/shared/design-system/tokens-table.mjs";
+import {
+ DEPRECATED_SYSTEM_COLORS,
+ NAMED_COLORS,
+ PREFIXED_SYSTEM_COLORS,
+ SYSTEM_COLORS,
+} from "./referenceColors.mjs";
/**
* Allows rules to access the tokens table without hard-coding the import path in multiple files.
@@ -162,6 +168,35 @@ export const isVariableFunction = node =>
isFunction(node) && node.value === "var";
/**
+ * Checks if CSS includes a named color, e.g. 'white' or 'rebeccapurple'
+ *
+ * @param {string} value some CSS declaration to match
+ * @returns {boolean}
+ */
+export const containsNamedColor = value =>
+ valueParser(String(value)).nodes.some(
+ node =>
+ node.type === "word" && NAMED_COLORS.includes(node.value.toLowerCase())
+ );
+
+/**
+ * Checks if CSS includes a named color, e.g. 'white' or 'rebeccapurple'
+ *
+ * @param {string} value some CSS declaration to match
+ * @returns {boolean}
+ */
+export const containsSystemColor = value =>
+ valueParser(String(value)).nodes.some(
+ node =>
+ node.type === "word" &&
+ [
+ ...PREFIXED_SYSTEM_COLORS,
+ ...DEPRECATED_SYSTEM_COLORS,
+ ...SYSTEM_COLORS,
+ ].includes(node.value.toLowerCase())
+ );
+
+/**
* Checks if CSS includes a hex value, e.g. `#00000`.
*
* @param {string} value some CSS declaration to match
@@ -189,6 +224,33 @@ export const containsColorFunction = value => {
};
/**
+ * Returns only the properties in the declaration that are colors, or at least likely to be colors.
+ * This allows for ignoring properties in shorthand that are not relevant to color rules.
+ *
+ * @param {string} value some CSS declaration to match
+ * @returns {string[]}
+ */
+export const getColorProperties = value => {
+ const relevantProperties = [];
+ const parsed = valueParser(value);
+ parsed.nodes.forEach(node => {
+ const property = value.substring(node.sourceIndex, node.sourceEndIndex);
+ if (
+ ALLOW_LIST.includes(property) ||
+ containsHexColor(property) ||
+ containsNamedColor(property) ||
+ containsSystemColor(property) ||
+ containsColorFunction(property) ||
+ isVariableFunction(node)
+ ) {
+ relevantProperties.push(property);
+ }
+ });
+
+ return relevantProperties;
+};
+
+/**
* Looks to see if a value is included in our token var() array.
*
* @param {string} value some CSS declaration to match
diff --git a/tools/lint/stylelint/stylelint-plugin-mozilla/referenceColors.mjs b/tools/lint/stylelint/stylelint-plugin-mozilla/referenceColors.mjs
@@ -0,0 +1,258 @@
+/**
+ * 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/.
+ */
+
+/**
+ * The list of named colors in CSS.
+ */
+export const NAMED_COLORS = [
+ "aliceblue",
+ "antiquewhite",
+ "aqua",
+ "aquamarine",
+ "azure",
+ "beige",
+ "bisque",
+ "black",
+ "blanchedalmond",
+ "blue",
+ "blueviolet",
+ "brown",
+ "burlywood",
+ "cadetblue",
+ "chartreuse",
+ "chocolate",
+ "coral",
+ "cornflowerblue",
+ "cornsilk",
+ "crimson",
+ "cyan",
+ "darkblue",
+ "darkcyan",
+ "darkgoldenrod",
+ "darkgray",
+ "darkgreen",
+ "darkgrey",
+ "darkkhaki",
+ "darkmagenta",
+ "darkolivegreen",
+ "darkorange",
+ "darkorchid",
+ "darkred",
+ "darksalmon",
+ "darkseagreen",
+ "darkslateblue",
+ "darkslategray",
+ "darkslategrey",
+ "darkturquoise",
+ "darkviolet",
+ "deeppink",
+ "deepskyblue",
+ "dimgray",
+ "dimgrey",
+ "dodgerblue",
+ "firebrick",
+ "floralwhite",
+ "forestgreen",
+ "fuchsia",
+ "gainsboro",
+ "ghostwhite",
+ "gold",
+ "goldenrod",
+ "gray",
+ "green",
+ "greenyellow",
+ "grey",
+ "honeydew",
+ "hotpink",
+ "indianred",
+ "indigo",
+ "ivory",
+ "khaki",
+ "lavender",
+ "lavenderblush",
+ "lawngreen",
+ "lemonchiffon",
+ "lightblue",
+ "lightcoral",
+ "lightcyan",
+ "lightgoldenrodyellow",
+ "lightgray",
+ "lightgreen",
+ "lightgrey",
+ "lightpink",
+ "lightsalmon",
+ "lightseagreen",
+ "lightskyblue",
+ "lightslategray",
+ "lightslategrey",
+ "lightsteelblue",
+ "lightyellow",
+ "lime",
+ "limegreen",
+ "linen",
+ "magenta",
+ "maroon",
+ "mediumaquamarine",
+ "mediumblue",
+ "mediumorchid",
+ "mediumpurple",
+ "mediumseagreen",
+ "mediumslateblue",
+ "mediumspringgreen",
+ "mediumturquoise",
+ "mediumvioletred",
+ "midnightblue",
+ "mintcream",
+ "mistyrose",
+ "moccasin",
+ "navajowhite",
+ "navy",
+ "oldlace",
+ "olive",
+ "olivedrab",
+ "orange",
+ "orangered",
+ "orchid",
+ "palegoldenrod",
+ "palegreen",
+ "paleturquoise",
+ "palevioletred",
+ "papayawhip",
+ "peachpuff",
+ "peru",
+ "pink",
+ "plum",
+ "powderblue",
+ "purple",
+ "rebeccapurple",
+ "red",
+ "rosybrown",
+ "royalblue",
+ "saddlebrown",
+ "salmon",
+ "sandybrown",
+ "seagreen",
+ "seashell",
+ "sienna",
+ "silver",
+ "skyblue",
+ "slateblue",
+ "slategray",
+ "slategrey",
+ "snow",
+ "springgreen",
+ "steelblue",
+ "tan",
+ "teal",
+ "thistle",
+ "tomato",
+ "turquoise",
+ "violet",
+ "wheat",
+ "white",
+ "whitesmoke",
+ "yellow",
+ "yellowgreen",
+];
+
+/**
+ * The list of browser prefixed system colors.
+ */
+export const PREFIXED_SYSTEM_COLORS = [
+ "-moz-buttondefault",
+ "-moz-buttonhoverface",
+ "-moz-buttonhovertext",
+ "-moz-cellhighlight",
+ "-moz-cellhighlighttext",
+ "-moz-combobox",
+ "-moz-comboboxtext",
+ "-moz-dialog",
+ "-moz-dialogtext",
+ "-moz-dragtargetzone",
+ "-moz-eventreerow",
+ "-moz-field",
+ "-moz-fieldtext",
+ "-moz-html-cellhighlight",
+ "-moz-html-cellhighlighttext",
+ "-moz-mac-accentdarkestshadow",
+ "-moz-mac-accentdarkshadow",
+ "-moz-mac-accentface",
+ "-moz-mac-accentlightesthighlight",
+ "-moz-mac-accentlightshadow",
+ "-moz-mac-accentregularhighlight",
+ "-moz-mac-accentregularshadow",
+ "-moz-mac-chrome-active",
+ "-moz-mac-chrome-inactive",
+ "-moz-mac-focusring",
+ "-moz-mac-menuselect",
+ "-moz-mac-menushadow",
+ "-moz-mac-menutextselect",
+ "-moz-menubarhovertext",
+ "-moz-menubartext",
+ "-moz-menuhover",
+ "-moz-menuhovertext",
+ "-moz-nativehyperlinktext",
+ "-moz-oddtreerow",
+ "-moz-win-accentcolor",
+ "-moz-win-accentcolortext",
+ "-moz-win-communicationstext",
+ "-moz-win-mediatext",
+ "-ms-hotlight",
+];
+
+/**
+ * The list of system colors that are deprecated, but may still be in use.
+ */
+export const DEPRECATED_SYSTEM_COLORS = [
+ "activeborder",
+ "activecaption",
+ "appworkspace",
+ "background",
+ "buttonhighlight",
+ "buttonshadow",
+ "captiontext",
+ "inactiveborder",
+ "inactivecaption",
+ "inactivecaptiontext",
+ "infobackground",
+ "infotext",
+ "menu",
+ "menutext",
+ "scrollbar",
+ "threeddarkshadow",
+ "threedface",
+ "threedhighlight",
+ "threedlightshadow",
+ "threedshadow",
+ "window",
+ "windowframe",
+ "windowtext",
+];
+
+/**
+ * The list of system colors that are valid and intended to be used for high contrast/forced colors mode situations.
+ */
+export const SYSTEM_COLORS = [
+ "accentcolor",
+ "accentcolortext",
+ "activetext",
+ "buttonborder",
+ "buttonface",
+ "buttontext",
+ "canvas",
+ "canvastext",
+ "field",
+ "fieldtext",
+ "graytext",
+ "highlight",
+ "highlighttext",
+ "linktext",
+ "mark",
+ "marktext",
+ "selecteditem",
+ "selecteditemtext",
+ "visitedtext",
+];
diff --git a/tools/lint/stylelint/stylelint-plugin-mozilla/rules/index.mjs b/tools/lint/stylelint/stylelint-plugin-mozilla/rules/index.mjs
@@ -10,6 +10,7 @@ import useBorderColorTokens from "./use-border-color-tokens.mjs";
import useFontSizeTokens from "./use-font-size-tokens.mjs";
import useFontWeightTokens from "./use-font-weight-tokens.mjs";
import useSpaceTokens from "./use-space-tokens.mjs";
+import useBackgroundColorTokens from "./use-background-color-tokens.mjs";
export default {
"no-base-design-tokens": noBaseDesignTokens,
@@ -18,4 +19,5 @@ export default {
"use-font-size-tokens": useFontSizeTokens,
"use-font-weight-tokens": useFontWeightTokens,
"use-space-tokens": useSpaceTokens,
+ "use-background-color-tokens": useBackgroundColorTokens,
};
diff --git a/tools/lint/stylelint/stylelint-plugin-mozilla/rules/use-background-color-tokens.mjs b/tools/lint/stylelint/stylelint-plugin-mozilla/rules/use-background-color-tokens.mjs
@@ -0,0 +1,94 @@
+/* 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/. */
+
+import stylelint from "stylelint";
+import {
+ namespace,
+ createTokenNamesArray,
+ createAllowList,
+ getLocalCustomProperties,
+ getColorProperties,
+ isValidValue,
+} from "../helpers.mjs";
+
+const {
+ utils: { report, ruleMessages, validateOptions },
+} = stylelint;
+
+// Name our rule, set the error message, and link to meta
+const ruleName = namespace("use-background-color-tokens");
+
+const messages = ruleMessages(ruleName, {
+ rejected: value => `${value} should use a background-color design token.`,
+});
+
+const meta = {
+ url: "https://firefox-source-docs.mozilla.org/code-quality/lint/linters/stylelint-plugin-mozilla/rules/use-background-color-tokens.html",
+ fixable: false,
+};
+
+// Gather an array of the ready css `['var(--token-name)']`
+const INCLUDE_CATEGORIES = ["background-color"];
+
+const tokenCSS = createTokenNamesArray(INCLUDE_CATEGORIES);
+
+// Allowed border-color values in CSS
+const ALLOW_LIST = createAllowList([
+ "transparent",
+ "currentColor",
+ "auto",
+ "normal",
+ "none",
+]);
+
+const CSS_PROPERTIES = ["background", "background-color"];
+
+const ruleFunction = primaryOption => {
+ return (root, result) => {
+ const validOptions = validateOptions(result, ruleName, {
+ actual: primaryOption,
+ possible: [true],
+ });
+
+ if (!validOptions) {
+ return;
+ }
+
+ // The first time through gathers our custom properties
+ const cssCustomProperties = getLocalCustomProperties(root);
+
+ // And then we validate our properties
+ root.walkDecls(declarations => {
+ const { prop, value } = declarations;
+
+ // If the property is not in our list to check, skip it
+ if (!CSS_PROPERTIES.includes(prop)) {
+ return;
+ }
+
+ // This rule only cares about colors, so all other shorthand properties are ignored
+ const colorProperties = getColorProperties(value);
+ const allColorsAreValid = colorProperties.every(property =>
+ isValidValue(property, tokenCSS, cssCustomProperties, ALLOW_LIST)
+ );
+
+ if (allColorsAreValid) {
+ return;
+ }
+
+ report({
+ message: messages.rejected(declarations.value),
+ node: declarations,
+ result,
+ ruleName,
+ });
+ });
+ };
+};
+
+ruleFunction.ruleName = ruleName;
+ruleFunction.messages = messages;
+ruleFunction.meta = meta;
+
+export default ruleFunction;
diff --git a/tools/lint/stylelint/stylelint-plugin-mozilla/tests/use-background-color-tokens.tests.mjs b/tools/lint/stylelint/stylelint-plugin-mozilla/tests/use-background-color-tokens.tests.mjs
@@ -0,0 +1,328 @@
+/**
+ * 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/PL/2.0/.
+ */
+
+// Bug 1948378: remove this exception when the eslint import plugin fully
+// supports exports in package.json files
+// eslint-disable-next-line import/no-unresolved
+import { testRule } from "stylelint-test-rule-node";
+import stylelint from "stylelint";
+import useBackgroundColorTokens from "../rules/use-background-color-tokens.mjs";
+
+let plugin = stylelint.createPlugin(
+ useBackgroundColorTokens.ruleName,
+ useBackgroundColorTokens
+);
+let {
+ ruleName,
+ rule: { messages },
+} = plugin;
+
+testRule({
+ plugins: [plugin],
+ ruleName,
+ config: true,
+ fix: false,
+ accept: [
+ {
+ code: ".bg { background-color: var(--background-color-box); }",
+ description: "Using background-color token is valid.",
+ },
+ {
+ code: ".bg { background-color: var(--background-color-box, #666); }",
+ description:
+ "Using background-color token with a raw color fallback is valid.",
+ },
+ {
+ code: ".bg { background-color: var(--background-color-box, var(--another-token)); }",
+ description:
+ "Using background-color token with a variable fallback is valid.",
+ },
+ {
+ code: `
+ :root { --custom-token: var(--background-color-box); }
+ .bg { background-color: var(--custom-token); }
+ `,
+ description:
+ "Using a custom token that resolves to a background-color token is valid.",
+ },
+ {
+ code: ".bg { background-color: inherit; }",
+ description: "Using a keyword is valid.",
+ },
+ {
+ code: ".bg { background-color: initial; }",
+ description: "Using a keyword is valid.",
+ },
+ {
+ code: ".bg { background-color: revert; }",
+ description: "Using a keyword is valid.",
+ },
+ {
+ code: ".bg { background-color: revert-layer; }",
+ description: "Using a keyword is valid.",
+ },
+ {
+ code: ".bg { background-color: unset; }",
+ description: "Using a keyword is valid.",
+ },
+ {
+ code: ".bg { background-color: transparent; }",
+ description: "Using a keyword is valid.",
+ },
+ {
+ code: ".bg { background-color: currentColor; }",
+ description: "Using a keyword is valid.",
+ },
+ {
+ code: ".bg { background-color: auto; }",
+ description: "Using a keyword is valid.",
+ },
+ {
+ code: ".bg { background-color: normal; }",
+ description: "Using a keyword is valid.",
+ },
+ {
+ code: ".bg { background-color: none; }",
+ description: "Using a keyword is valid.",
+ },
+ {
+ code: ".bg { background: var(--background-color-box); }",
+ description: "Using background-color token is valid in the shorthand.",
+ },
+ {
+ code: ".bg { background: var(--background-color-box, #666); }",
+ description:
+ "Using background-color token with a raw color fallback is valid in the shorthand.",
+ },
+ {
+ code: ".bg { background: var(--background-color-box, var(--another-token)); }",
+ description:
+ "Using background-color token with a token fallback is valid in the shorthand.",
+ },
+ {
+ code: `
+ :root { --custom-token: var(--background-color-box); }
+ .bg { background: var(--custom-token); }
+ `,
+ description:
+ "Using a custom token that resolves to a background-color token is valid in the shorthand.",
+ },
+ {
+ code: ".bg { background: url('image.png'); }",
+ description:
+ "Using the background shorthand without any color declarations is valid.",
+ },
+ {
+ code: ".bg { background: linear-gradient(to bottom, #fff, #000) var(--background-color-box); }",
+ description:
+ "Using the background shorthand, other properties plus a background-color token is valid.",
+ },
+ {
+ code: ".bg { background: url('image.png') no-repeat center center / auto var(--background-color-box, oklch(69% 0.19 15)); }",
+ description:
+ "Using a background-color token with a raw color value fallback is valid in the shorthand.",
+ },
+ {
+ code: ".bg { background: url('image.png') fixed content-box var(--background-color-box, var(--another-token)); }",
+ description:
+ "Using a background-color token with another token fallback is valid in the shorthand.",
+ },
+ {
+ code: `
+ :root { --custom-token: var(--background-color-box); }
+ .bg { background: url('image.png') var(--custom-token) repeat-y fixed; }
+ `,
+ description:
+ "Using a custom token that resolves to a background-color token is valid in the shorthand.",
+ },
+ {
+ code: ".bg { background: inherit; }",
+ description: "Using a keyword is valid in the shorthand.",
+ },
+ {
+ code: ".bg { background: initial; }",
+ description: "Using a keyword is valid in the shorthand.",
+ },
+ {
+ code: ".bg { background: revert; }",
+ description: "Using a keyword is valid in the shorthand.",
+ },
+ {
+ code: ".bg { background: revert-layer; }",
+ description: "Using a keyword is valid in the shorthand.",
+ },
+ {
+ code: ".bg { background: unset; }",
+ description: "Using a keyword is valid in the shorthand.",
+ },
+ {
+ code: ".bg { background: transparent; }",
+ description: "Using a keyword is valid in the shorthand.",
+ },
+ {
+ code: ".bg { background: currentColor; }",
+ description: "Using a keyword is valid in the shorthand.",
+ },
+ {
+ code: ".bg { background: auto; }",
+ description: "Using a keyword is valid in the shorthand.",
+ },
+ {
+ code: ".bg { background: normal; }",
+ description: "Using a keyword is valid in the shorthand.",
+ },
+ {
+ code: ".bg { background: none; }",
+ description: "Using a keyword is valid in the shorthand.",
+ },
+ ],
+
+ reject: [
+ {
+ code: ".bg { background-color: #666; }",
+ message: messages.rejected("#666"),
+ description: "#666 should use a background-color design token.",
+ },
+ {
+ code: ".bg { background-color: #fff0; }",
+ message: messages.rejected("#fff0"),
+ description: "#fff0 should use a background-color design token.",
+ },
+ {
+ code: ".bg { background-color: #666666; }",
+ message: messages.rejected("#666666"),
+ description: "#666666 should use a background-color design token.",
+ },
+ {
+ code: ".bg { background-color: #ffffff00; }",
+ message: messages.rejected("#ffffff00"),
+ description: "#ffffff00 should use a background-color design token.",
+ },
+ {
+ code: ".bg { background-color: oklch(69% 0.19 15); }",
+ message: messages.rejected("oklch(69% 0.19 15)"),
+ description:
+ "oklch(69% 0.19 15) should use a background-color design token.",
+ },
+ {
+ code: ".bg { background-color: rgba(42 42 42 / 0.15); }",
+ message: messages.rejected("rgba(42 42 42 / 0.15)"),
+ description:
+ "rgba(42 42 42 / 0.15) should use a background-color design token.",
+ },
+ {
+ code: ".bg { background-color: ButtonFace; }",
+ message: messages.rejected("ButtonFace"),
+ description: "ButtonFace should use a background-color design token.",
+ },
+ {
+ code: ".bg { background-color: var(--random-token, oklch(69% 0.19 15)); }",
+ message: messages.rejected("var(--random-token, oklch(69% 0.19 15))"),
+ description:
+ "var(--random-token, oklch(69% 0.19 15)) should use a background-color design token.",
+ },
+ {
+ code: `
+ :root { --custom-token: #666; }
+ .bg { background-color: var(--custom-token); }
+ `,
+ message: messages.rejected("var(--custom-token)"),
+ description:
+ "var(--custom-token) should use a background-color design token.",
+ },
+ {
+ code: ".bg { background: #666; }",
+ message: messages.rejected("#666"),
+ description: "#666 should use a background-color design token.",
+ },
+ {
+ code: ".bg { background: #fff0; }",
+ message: messages.rejected("#fff0"),
+ description: "#fff0 should use a background-color design token.",
+ },
+ {
+ code: ".bg { background: #666666; }",
+ message: messages.rejected("#666666"),
+ description: "#666666 should use a background-color design token.",
+ },
+ {
+ code: ".bg { background: #ffffff00; }",
+ message: messages.rejected("#ffffff00"),
+ description: "#ffffff00 should use a background-color design token.",
+ },
+ {
+ code: ".bg { background: oklch(69% 0.19 15); }",
+ message: messages.rejected("oklch(69% 0.19 15)"),
+ description:
+ "oklch(69% 0.19 15) should use a background-color design token.",
+ },
+ {
+ code: ".bg { background: rgba(42 42 42 / 0.15); }",
+ message: messages.rejected("rgba(42 42 42 / 0.15)"),
+ description:
+ "rgba(42 42 42 / 0.15) should use a background-color design token.",
+ },
+ {
+ code: ".bg { background: border-box #666; }",
+ message: messages.rejected("border-box #666"),
+ description:
+ "border-box #666 should use a background-color design token.",
+ },
+ {
+ code: ".bg { background: url('image.png') #fff0, #666; }",
+ message: messages.rejected("url('image.png') #fff0, #666"),
+ description:
+ "url('image.png') #fff0, #666 should use a background-color design token.",
+ },
+ {
+ code: ".bg { background: url('image.png') oklch(69% 0.19 15) repeat-y; }",
+ message: messages.rejected(
+ "url('image.png') oklch(69% 0.19 15) repeat-y"
+ ),
+ description:
+ "url('image.png') oklch(69% 0.19 15) repeat-y should use a background-color design token.",
+ },
+ {
+ code: ".bg { background: url('image.png') top center fixed #ffffff00; }",
+ message: messages.rejected("url('image.png') top center fixed #ffffff00"),
+ description:
+ "url('image.png') top center fixed #ffffff00 should use a background-color design token.",
+ },
+ {
+ code: ".bg { background: url('image.png') center left / auto no-repeat scroll content-box padding-box red, rgba(42 42 42 / 0.15); }",
+ message: messages.rejected(
+ "url('image.png') center left / auto no-repeat scroll content-box padding-box red, rgba(42 42 42 / 0.15)"
+ ),
+ description:
+ "url('image.png') center left / auto no-repeat scroll content-box padding-box red, rgba(42 42 42 / 0.15) should use a background-color design token.",
+ },
+ {
+ code: ".bg { background: url('image.png') var(--random-token, rgba(42 42 42 / 0.15)); }",
+ message: messages.rejected(
+ "url('image.png') var(--random-token, rgba(42 42 42 / 0.15))"
+ ),
+ description:
+ "url('image.png') var(--random-token, rgba(42 42 42 / 0.15)) should use a background-color design token.",
+ },
+ {
+ code: ".bg { background: url('image.png') Canvas; }",
+ message: messages.rejected("url('image.png') Canvas"),
+ description:
+ "url('image.png') Canvas should use a background-color design token.",
+ },
+ {
+ code: `
+ :root { --custom-token: #666666; }
+ .bg { background: url('image.png') no-repeat center / auto var(--custom-token); }
+ `,
+ message: messages.rejected(
+ "url('image.png') no-repeat center / auto var(--custom-token)"
+ ),
+ description:
+ "url('image.png') no-repeat center / auto var(--custom-token) should use a background-color design token.",
+ },
+ ],
+});