commit 9ac0c56f0e8c4e169be9d68d0ce2d1352aab7f9f
parent c09107d2916a4c4c5dc425e5b3f454f1686ad837
Author: Jon Oliver <jooliver@mozilla.com>
Date: Fri, 17 Oct 2025 16:56:49 +0000
Bug 1988864 - add box-shadow stylelint rule r=frontend-codestyle-reviewers,hjones
- Add use-box-shadow-tokens stylelint rule
- Add tests for use-box-shadow-tokens rule
- Add docs for use-box-shadow-tokens rule
- Update stylelint rollouts with exclusions for use-box-shadow-tokens rule
Differential Revision: https://phabricator.services.mozilla.com/D268200
Diffstat:
6 files changed, 406 insertions(+), 0 deletions(-)
diff --git a/.stylelintrc.js b/.stylelintrc.js
@@ -277,6 +277,7 @@ module.exports = {
"stylelint-plugin-mozilla/use-font-weight-tokens": true,
"stylelint-plugin-mozilla/use-space-tokens": true,
"stylelint-plugin-mozilla/use-text-color-tokens": true,
+ "stylelint-plugin-mozilla/use-box-shadow-tokens": true,
},
overrides: [
@@ -424,6 +425,7 @@ module.exports = {
"stylelint-plugin-mozilla/use-font-weight-tokens": false,
"stylelint-plugin-mozilla/use-space-tokens": false,
"stylelint-plugin-mozilla/use-text-color-tokens": false,
+ "stylelint-plugin-mozilla/use-box-shadow-tokens": false,
},
},
{
diff --git a/docs/code-quality/lint/linters/stylelint-plugin-mozilla/rules/use-box-shadow-tokens.rst b/docs/code-quality/lint/linters/stylelint-plugin-mozilla/rules/use-box-shadow-tokens.rst
@@ -0,0 +1,112 @@
+========================
+use-box-shadow-tokens
+========================
+
+This rule checks that CSS declarations use box-shadow design token variables
+instead of hardcoded values. This ensures consistent box-shadow usage across
+the application and makes it easier to maintain design system consistency.
+
+Examples of incorrect code for this rule:
+-----------------------------------------
+
+.. code-block:: css
+
+ .button {
+ box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
+ }
+
+.. code-block:: css
+
+ .element {
+ box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.1);
+ }
+
+Examples of correct token usage for this rule:
+----------------------------------------------
+
+.. code-block:: css
+
+ .card {
+ box-shadow: var(--box-shadow-card);
+ }
+
+.. code-block:: css
+
+ .card-hover {
+ box-shadow: var(--box-shadow-card-hover);
+ }
+
+.. code-block:: css
+
+ .level-1-shadow {
+ box-shadow: var(--box-shadow-level-1);
+ }
+
+.. code-block:: css
+
+ .level-2-shadow {
+ box-shadow: var(--box-shadow-level-2);
+ }
+
+.. code-block:: css
+
+ .level-3-shadow {
+ box-shadow: var(--box-shadow-level-3);
+ }
+
+.. code-block:: css
+
+ .level-4-shadow {
+ box-shadow: var(--box-shadow-level-4);
+ }
+
+.. code-block:: css
+
+ .popup {
+ box-shadow: var(--box-shadow-popup);
+ }
+
+.. code-block:: css
+
+ .tab {
+ box-shadow: var(--box-shadow-tab);
+ }
+
+
+The rule also allows these non-token values:
+
+.. code-block:: css
+
+ .inherited-shadow {
+ box-shadow: inherit;
+ }
+
+.. code-block:: css
+
+ .initial-shadow {
+ box-shadow: initial;
+ }
+
+.. code-block:: css
+
+ .revert-shadow {
+ box-shadow: revert;
+ }
+
+.. code-block:: css
+
+ .revert-layer-shadow {
+ box-shadow: revert-layer;
+ }
+
+.. code-block:: css
+
+ .unset-shadow {
+ box-shadow: unset;
+ }
+
+.. code-block:: css
+
+ .no-shadow {
+ box-shadow: none;
+ }
diff --git a/stylelint-rollouts.config.js b/stylelint-rollouts.config.js
@@ -1393,4 +1393,79 @@ module.exports = [
"tools/tryselect/selectors/chooser/static/style.css",
],
},
+ {
+ // stylelint fixes for this rule will be addressed in Bug 1993565
+ name: "rollout-use-box-shadow-tokens",
+ rules: {
+ "stylelint-plugin-mozilla/use-box-shadow-tokens": null,
+ },
+ files: [
+ "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-message-popup.css",
+ "browser/components/aboutlogins/content/components/remove-logins-dialog.css",
+ "browser/components/aboutwelcome/content-src/aboutwelcome.scss",
+ "browser/components/asrouter/content-src/styles/_feature-callout.scss",
+ "browser/components/firefoxview/card-container.css",
+ "browser/components/firefoxview/history.css",
+ "browser/components/genai/content/model-optin.css",
+ "browser/components/profiles/content/profile-avatar-selector.css",
+ "browser/components/protections/content/protections.css",
+ "browser/components/screenshots/overlay/overlay.css",
+ "browser/components/search/content/contentSearchUI.css",
+ "browser/components/urlbar/tests/browser/dynamicResult0.css",
+ "browser/components/urlbar/tests/browser/dynamicResult1.css",
+ "browser/extensions/newtab/content-src/components/Card/_Card.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/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/DSTextPromo/_DSTextPromo.scss",
+ "browser/extensions/newtab/content-src/components/DiscoveryStreamComponents/FeatureHighlight/_DownloadMobilePromoHighlight.scss",
+ "browser/extensions/newtab/content-src/components/DiscoveryStreamComponents/FeatureHighlight/_FeatureHighlight.scss",
+ "browser/extensions/newtab/content-src/components/DiscoveryStreamComponents/FeatureHighlight/_WallpaperFeatureHighlight.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/ErrorBoundary/_ErrorBoundary.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/styles/_mixins.scss",
+ "browser/extensions/newtab/content-src/styles/_variables.scss",
+ "browser/extensions/newtab/content-src/styles/activity-stream.scss",
+ "browser/themes/shared/UITour.css",
+ "browser/themes/shared/addons/unified-extensions.css",
+ "browser/themes/shared/autocomplete.css",
+ "browser/themes/shared/browser-shared.css",
+ "browser/themes/shared/customizableui/customizeMode.css",
+ "browser/themes/shared/customizableui/panelUI-shared.css",
+ "browser/themes/shared/privatebrowsing/aboutPrivateBrowsing.css",
+ "browser/themes/shared/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/urlbar-searchbar.css",
+ "browser/themes/windows/places/organizer.css",
+ "gfx/layers/layerviewer/tree.css",
+ "layout/style/res/ua.css",
+ "toolkit/components/aboutinference/content/aboutInference.css",
+ "toolkit/content/aboutTelemetry.css",
+ "toolkit/content/widgets/infobar.css",
+ "toolkit/mozapps/extensions/content/shortcuts.css",
+ "toolkit/themes/shared/aboutReader.css",
+ "toolkit/themes/shared/alert.css",
+ "toolkit/themes/shared/findbar.css",
+ "toolkit/themes/shared/in-content/common-shared.css",
+ "toolkit/themes/shared/pictureinpicture/player.css",
+ "toolkit/themes/shared/popup.css",
+ "toolkit/themes/shared/toolbarbutton.css",
+ "toolkit/themes/shared/tree/tree.css",
+ ],
+ },
];
diff --git a/tools/lint/stylelint/stylelint-plugin-mozilla/rules/index.mjs b/tools/lint/stylelint/stylelint-plugin-mozilla/rules/index.mjs
@@ -12,6 +12,7 @@ import useFontWeightTokens from "./use-font-weight-tokens.mjs";
import useSpaceTokens from "./use-space-tokens.mjs";
import useBackgroundColorTokens from "./use-background-color-tokens.mjs";
import useTextColorTokens from "./use-text-color-tokens.mjs";
+import useBoxShadowTokens from "./use-box-shadow-tokens.mjs";
export default {
"no-base-design-tokens": noBaseDesignTokens,
@@ -22,4 +23,5 @@ export default {
"use-space-tokens": useSpaceTokens,
"use-background-color-tokens": useBackgroundColorTokens,
"use-text-color-tokens": useTextColorTokens,
+ "use-box-shadow-tokens": useBoxShadowTokens,
};
diff --git a/tools/lint/stylelint/stylelint-plugin-mozilla/rules/use-box-shadow-tokens.mjs b/tools/lint/stylelint/stylelint-plugin-mozilla/rules/use-box-shadow-tokens.mjs
@@ -0,0 +1,74 @@
+/* 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 {
+ createTokenNamesArray,
+ createAllowList,
+ getLocalCustomProperties,
+ isValidTokenUsage,
+ namespace,
+} from "../helpers.mjs";
+
+const {
+ utils: { report, ruleMessages, validateOptions },
+} = stylelint;
+
+const ruleName = namespace("use-box-shadow-tokens");
+
+const messages = ruleMessages(ruleName, {
+ rejected: value => `${value} should use a box-shadow design token.`,
+});
+
+const meta = {
+ url: "https://firefox-source-docs.mozilla.org/code-quality/lint/linters/stylelint-plugin-mozilla/rules/use-box-shadow-tokens.html",
+ fixable: false,
+};
+
+const PROPERTY_NAME = "box-shadow";
+
+const tokenCSS = createTokenNamesArray([PROPERTY_NAME]);
+
+const ALLOW_LIST = createAllowList(["none"]);
+
+const ruleFunction = primaryOption => {
+ return (root, result) => {
+ const validOptions = validateOptions(result, ruleName, {
+ actual: primaryOption,
+ possible: [true],
+ });
+
+ if (!validOptions) {
+ return;
+ }
+
+ const cssCustomProperties = getLocalCustomProperties(root);
+
+ root.walkDecls(PROPERTY_NAME, declarations => {
+ if (
+ isValidTokenUsage(
+ declarations.value,
+ tokenCSS,
+ cssCustomProperties,
+ ALLOW_LIST
+ )
+ ) {
+ 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-box-shadow-tokens.tests.mjs b/tools/lint/stylelint/stylelint-plugin-mozilla/tests/use-box-shadow-tokens.tests.mjs
@@ -0,0 +1,141 @@
+/**
+ * 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/.
+ */
+
+// 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 useBoxShadowTokens from "../rules/use-box-shadow-tokens.mjs";
+
+let plugin = stylelint.createPlugin(
+ useBoxShadowTokens.ruleName,
+ useBoxShadowTokens
+);
+let {
+ ruleName,
+ rule: { messages },
+} = plugin;
+
+testRule({
+ plugins: [plugin],
+ ruleName,
+ config: [true, { tokenType: "brand" }],
+ fix: false,
+ accept: [
+ // allowed token values
+ {
+ code: ".a { box-shadow: var(--box-shadow-card); }",
+ description: "Using box-shadow-card token is valid.",
+ },
+ {
+ code: ".a { box-shadow: var(--box-shadow-card-hover); }",
+ description: "Using box-shadow-card-hover token is valid.",
+ },
+ {
+ code: ".a { box-shadow: var(--box-shadow-level-1); }",
+ description: "Using box-shadow-level-1 token is valid.",
+ },
+ {
+ code: ".a { box-shadow: var(--box-shadow-level-2); }",
+ description: "Using box-shadow-level-2 token is valid.",
+ },
+ {
+ code: ".a { box-shadow: var(--box-shadow-level-3); }",
+ description: "Using box-shadow-level-3 token is valid.",
+ },
+ {
+ code: ".a { box-shadow: var(--box-shadow-level-4); }",
+ description: "Using box-shadow-level-4 token is valid.",
+ },
+ {
+ code: ".a { box-shadow: var(--box-shadow-popup); }",
+ description: "Using box-shadow-popup token is valid.",
+ },
+ {
+ code: ".a { box-shadow: var(--box-shadow-tab); }",
+ description: "Using box-shadow-tab token is valid.",
+ },
+ // allowed CSS values
+ {
+ code: ".a { box-shadow: inherit; }",
+ description: "Using inherit is valid.",
+ },
+ {
+ code: ".a { box-shadow: initial; }",
+ description: "Using initial is valid.",
+ },
+ {
+ code: ".a { box-shadow: revert; }",
+ description: "Using revert is valid.",
+ },
+ {
+ code: ".a { box-shadow: revert-layer; }",
+ description: "Using revert-layer is valid.",
+ },
+ {
+ code: ".a { box-shadow: unset; }",
+ description: "Using unset is valid.",
+ },
+ {
+ code: ".a { box-shadow: none; }",
+ description: "Using none keyword is valid.",
+ },
+ // fallbacks and custom properties
+ {
+ code: ".a { box-shadow:var(--my-local, var(--box-shadow-level-1)); }",
+ description:
+ "Using a custom property with fallback to design token is valid.",
+ },
+ {
+ code: `
+ :root { --custom-token: var(--box-shadow-card); }
+ .a { box-shadow: var(--custom-token); }
+ `,
+ description:
+ "Using a custom property with fallback to a design token is valid.",
+ },
+ ],
+
+ reject: [
+ {
+ code: ".a { box-shadow: 0 1px 3px rgba(0, 0, 0, 0.12); }",
+ message: messages.rejected("0 1px 3px rgba(0, 0, 0, 0.12)"),
+ description: "Using hardcoded box-shadow should use a design token.",
+ },
+ {
+ code: ".a { box-shadow: 0 1px 3px rgba(0, 0, 0, 0.12), 0 1px 2px rgba(0, 0, 0, 0.24); }",
+ message: messages.rejected(
+ "0 1px 3px rgba(0, 0, 0, 0.12), 0 1px 2px rgba(0, 0, 0, 0.24)"
+ ),
+ description: "Using multiple box-shadows should use a design token.",
+ },
+ {
+ code: ".a { box-shadow: calc(var(--my-local) + 1px) 2px 4px rgba(0, 0, 0, 0.1); }",
+ message: messages.rejected(
+ "calc(var(--my-local) + 1px) 2px 4px rgba(0, 0, 0, 0.1)"
+ ),
+ description:
+ "Using a calc() with custom variables should use a design token.",
+ },
+ {
+ code: ".a { box-shadow: var(--random-token, 0 2px 4px rgba(0, 0, 0, 0.1)); }",
+ message: messages.rejected(
+ "var(--random-token, 0 2px 4px rgba(0, 0, 0, 0.1))"
+ ),
+ description: "Using a custom property should use a design token.",
+ },
+ {
+ code: `
+ :root { --custom-token: 0 2px 4px rgba(0, 0, 0, 0.1); }
+ .a { box-shadow: var(--custom-token); }
+ `,
+ message: messages.rejected("var(--custom-token)"),
+ description:
+ "Using a custom property that does not resolve to a design token should use a design token.",
+ },
+ ],
+});