commit ef292b5ecbe5cec8aadeb61688b490871a34b912
parent 549157b501218c26a3cee2699fd137bc41a16619
Author: Jon Oliver <jooliver@mozilla.com>
Date: Wed, 8 Oct 2025 21:50:27 +0000
Bug 1988859 - add font-weight stylelint rule r=frontend-codestyle-reviewers,jules
- Add use-font-weight-tokens stylelint rule
- Add tests for use-font-weight-tokens rule
- Add docs for use-font-weight-tokens rule
- Update stylelint rollouts with exclusions for use-font-weight-tokens rule
Differential Revision: https://phabricator.services.mozilla.com/D267461
Diffstat:
6 files changed, 584 insertions(+), 0 deletions(-)
diff --git a/.stylelintrc.js b/.stylelintrc.js
@@ -273,6 +273,7 @@ module.exports = {
"stylelint-plugin-mozilla/use-border-radius-tokens": true,
"stylelint-plugin-mozilla/use-border-color-tokens": true,
"stylelint-plugin-mozilla/use-font-size-tokens": true,
+ "stylelint-plugin-mozilla/use-font-weight-tokens": true,
},
overrides: [
@@ -416,6 +417,7 @@ module.exports = {
"stylelint-plugin-mozilla/use-border-radius-tokens": false,
"stylelint-plugin-mozilla/use-border-color-tokens": false,
"stylelint-plugin-mozilla/use-font-size-tokens": false,
+ "stylelint-plugin-mozilla/use-font-weight-tokens": false,
},
},
{
diff --git a/docs/code-quality/lint/linters/stylelint-plugin-mozilla/rules/use-font-weight-tokens.rst b/docs/code-quality/lint/linters/stylelint-plugin-mozilla/rules/use-font-weight-tokens.rst
@@ -0,0 +1,166 @@
+========================
+use-font-weight-tokens
+========================
+
+This rule checks that CSS declarations use font-weight design token variables
+instead of hardcoded values. This ensures consistent font-weight usage across
+the application and makes it easier to maintain design system consistency.
+
+This rule is autofixable and can automatically replace some font-weight values
+with appropriate design tokens where possible.
+
+Examples of incorrect code for this rule:
+-----------------------------------------
+
+.. code-block:: css
+
+ .normal-text {
+ font-weight: normal;
+ }
+
+.. code-block:: css
+
+ .bold-text {
+ font-weight: bold;
+ }
+
+.. code-block:: css
+
+ .bolder-text {
+ font-weight: bolder;
+ }
+
+.. code-block:: css
+
+ .lighter-text {
+ font-weight: lighter;
+ }
+
+.. code-block:: css
+
+ .custom-text {
+ font-weight: 400;
+ }
+
+.. code-block:: css
+
+ .heading {
+ font-weight: 600;
+ }
+
+.. code-block:: css
+
+ .bold-text {
+ font-weight: 700;
+ }
+
+.. code-block:: css
+
+ .light-text {
+ font-weight: 300;
+ }
+
+.. code-block:: css
+
+ .heavy-text {
+ font-weight: 900;
+ }
+
+Examples of correct token usage for this rule:
+----------------------------------------------
+
+.. code-block:: css
+
+ .normal-text {
+ font-weight: var(--font-weight);
+ }
+
+.. code-block:: css
+
+ .bold-text {
+ font-weight: var(--font-weight-bold);
+ }
+
+.. code-block:: css
+
+ .button-text {
+ font-weight: var(--button-font-weight);
+ }
+
+.. code-block:: css
+
+ .heading-text {
+ font-weight: var(--heading-font-weight);
+ }
+
+.. code-block:: css
+
+ /* Local CSS variables that reference valid font-weight tokens are allowed */
+ :root {
+ --custom-font-weight: var(--font-weight-bold);
+ }
+
+ .custom-text {
+ font-weight: var(--custom-font-weight);
+ }
+
+.. code-block:: css
+
+ .custom-text {
+ font-weight: var(--custom-font-weight, var(--font-weight-bold));
+ }
+
+The rule also allows these non-token values:
+
+.. code-block:: css
+
+ .inherited-text {
+ font-weight: inherit;
+ }
+
+.. code-block:: css
+
+ .initial-text {
+ font-weight: initial;
+ }
+
+.. code-block:: css
+
+ .unset-text {
+ font-weight: unset;
+ }
+
+Autofix functionality
+---------------------
+
+This rule can automatically fix some violations by replacing values with
+appropriate design tokens:
+
+- ``normal`` → ``var(--font-weight)``
+- ``600`` → ``var(--font-weight-bold)``
+
+Examples of autofixable violations:
+
+.. code-block:: css
+
+ /* Before */
+ .normal-text {
+ font-weight: normal;
+ }
+
+ /* After autofix */
+ .normal-text {
+ font-weight: var(--font-weight);
+ }
+
+.. code-block:: css
+
+ /* Before */
+ .bold-text {
+ font-weight: 600;
+ }
+
+ /* After autofix */
+ .bold-text {
+ font-weight: var(--font-weight-bold);
+ }
diff --git a/stylelint-rollouts.config.js b/stylelint-rollouts.config.js
@@ -462,4 +462,136 @@ module.exports = [
"tools/tryselect/selectors/chooser/static/style.css",
],
},
+ {
+ // stylelint fixes for this rule will be addressed in Bug 1992736
+ name: "rollout-use-font-weight-tokens",
+ rules: {
+ "stylelint-plugin-mozilla/use-font-weight-tokens": null,
+ },
+ files: [
+ "browser/base/content/aboutDialog.css",
+ "browser/base/content/safeMode.css",
+ "browser/branding/aurora/stubinstaller/profile_cleanup_page.css",
+ "browser/branding/nightly/stubinstaller/profile_cleanup_page.css",
+ "browser/branding/official/stubinstaller/installing_page.css",
+ "browser/branding/official/stubinstaller/profile_cleanup_page.css",
+ "browser/branding/unofficial/stubinstaller/profile_cleanup_page.css",
+ "browser/components/aboutlogins/content/aboutLogins.css",
+ "browser/components/aboutlogins/content/aboutLoginsImportReport.css",
+ "browser/components/aboutlogins/content/components/confirmation-dialog.css",
+ "browser/components/aboutlogins/content/components/import-error-dialog.css",
+ "browser/components/aboutlogins/content/components/import-summary-dialog.css",
+ "browser/components/aboutlogins/content/components/login-alert.css",
+ "browser/components/aboutlogins/content/components/login-list-lit-item.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/backup/content/password-rules-tooltip.css",
+ "browser/components/backup/content/password-validation-inputs.css",
+ "browser/components/backup/content/restore-from-backup.css",
+ "browser/components/backup/content/turn-on-scheduled-backups.css",
+ "browser/components/firefoxview/card-container.css",
+ "browser/components/firefoxview/fxview-tab-row.css",
+ "browser/components/firefoxview/history.css",
+ "browser/components/firefoxview/view-syncedtabs.css",
+ "browser/components/genai/content/smart-assist.css",
+ "browser/components/protections/content/protections.css",
+ "browser/components/search/content/contentSearchUI.css",
+ "browser/components/tabunloader/content/aboutUnloads.css",
+ "browser/components/textrecognition/textrecognition.css",
+ "browser/extensions/webcompat/about-compat/aboutCompat.css",
+ "browser/themes/linux/browser.css",
+ "browser/themes/shared/UITour.css",
+ "browser/themes/shared/aboutTabCrashed.css",
+ "browser/themes/shared/addon-notification.css",
+ "browser/themes/shared/addons/extension-controlled.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/identity-credential-notification.css",
+ "browser/themes/shared/migration/migration-wizard.css",
+ "browser/themes/shared/pageInfo.css",
+ "browser/themes/shared/places/editBookmarkPanel.css",
+ "browser/themes/shared/preferences/fxaPairDevice.css",
+ "browser/themes/shared/preferences/preferences.css",
+ "browser/themes/shared/preferences/privacy.css",
+ "browser/themes/shared/preferences/siteDataSettings.css",
+ "browser/themes/shared/privatebrowsing/aboutPrivateBrowsing.css",
+ "browser/themes/shared/search/searchbar.css",
+ "browser/themes/shared/setDesktopBackground.css",
+ "browser/themes/shared/tabbrowser/ctrlTab.css",
+ "browser/themes/shared/tabbrowser/fullscreen-and-pointerlock.css",
+ "browser/themes/shared/tabbrowser/tabs.css",
+ "browser/themes/shared/translations/panel.css",
+ "browser/themes/shared/urlbar-dynamic-results.css",
+ "browser/themes/shared/urlbar-searchbar.css",
+ "browser/themes/windows/browser.css",
+ "dom/crypto/test/test_WebCrypto.css",
+ "dom/events/test/pointerevents/wpt/pointerevent_styles.css",
+ "dom/xml/test/old/books/classic.css",
+ "dom/xml/test/old/books/common.css",
+ "dom/xml/test/old/books/list.css",
+ "dom/xml/test/old/xmlbase/xmlbase.css",
+ "layout/mathml/mathml.css",
+ "layout/style/res/forms.css",
+ "layout/style/res/html.css",
+ "layout/style/res/ua.css",
+ "layout/style/res/viewsource.css",
+ "security/manager/pki/resources/content/clientauthask.css",
+ "security/manager/pki/resources/content/exceptionDialog.css",
+ "testing/mozbase/mozlog/mozlog/formatters/html/style.css",
+ "toolkit/components/aboutconfig/content/aboutconfig.css",
+ "toolkit/components/aboutinference/content/aboutInference.css",
+ "toolkit/components/aboutmemory/content/aboutMemory.css",
+ "toolkit/components/aboutprocesses/content/aboutProcesses.css",
+ "toolkit/components/aboutwebauthn/content/aboutWebauthn.css",
+ "toolkit/components/aboutwindowsmessages/content/aboutWindowsMessages.css",
+ "toolkit/components/certviewer/content/components/info-group.css",
+ "toolkit/components/certviewer/content/components/info-item.css",
+ "toolkit/components/normandy/content/about-studies/about-studies.css",
+ "toolkit/content/aboutLogging/aboutLogging.css",
+ "toolkit/content/aboutTelemetry.css",
+ "toolkit/content/aboutUrlClassifier.css",
+ "toolkit/content/aboutwebrtc/aboutWebrtc.css",
+ "toolkit/content/resetProfile.css",
+ "toolkit/content/widgets/moz-box-common.css",
+ "toolkit/content/widgets/moz-message-bar/moz-message-bar.css",
+ "toolkit/content/widgets/moz-page-nav/moz-page-nav-button.css",
+ "toolkit/mozapps/extensions/content/aboutaddons.css",
+ "toolkit/mozapps/extensions/content/shortcuts.css",
+ "toolkit/mozapps/handling/content/handler.css",
+ "toolkit/themes/linux/mozapps/update/updates.css",
+ "toolkit/themes/mobile/global/aboutMemory.css",
+ "toolkit/themes/mobile/global/aboutNetworking.css",
+ "toolkit/themes/mobile/global/aboutSupport.css",
+ "toolkit/themes/osx/global/wizard.css",
+ "toolkit/themes/osx/mozapps/handling/handling.css",
+ "toolkit/themes/osx/mozapps/update/updates.css",
+ "toolkit/themes/shared/aboutHttpsOnlyError.css",
+ "toolkit/themes/shared/aboutNetError.css",
+ "toolkit/themes/shared/aboutReader.css",
+ "toolkit/themes/shared/aboutSupport.css",
+ "toolkit/themes/shared/alert.css",
+ "toolkit/themes/shared/appPicker.css",
+ "toolkit/themes/shared/commonDialog.css",
+ "toolkit/themes/shared/datetimeinputpickers.css",
+ "toolkit/themes/shared/dirListing/dirListing.css",
+ "toolkit/themes/shared/downloads/unknownContentType.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/menulist.css",
+ "toolkit/themes/shared/toolbarbutton.css",
+ "toolkit/themes/windows/global/global.css",
+ "toolkit/themes/windows/global/wizard.css",
+ "toolkit/themes/windows/mozapps/handling/handling.css",
+ "toolkit/themes/windows/mozapps/update/updates.css",
+ ],
+ },
];
diff --git a/tools/lint/stylelint/stylelint-plugin-mozilla/rules/index.mjs b/tools/lint/stylelint/stylelint-plugin-mozilla/rules/index.mjs
@@ -8,10 +8,12 @@ import noBaseDesignTokens from "./no-base-design-tokens.mjs";
import useBorderRadiusTokens from "./use-border-radius-tokens.mjs";
import useBorderColorTokens from "./use-border-color-tokens.mjs";
import useFontSizeTokens from "./use-font-size-tokens.mjs";
+import useFontWeightTokens from "./use-font-weight-tokens.mjs";
export default {
"no-base-design-tokens": noBaseDesignTokens,
"use-border-radius-tokens": useBorderRadiusTokens,
"use-border-color-tokens": useBorderColorTokens,
"use-font-size-tokens": useFontSizeTokens,
+ "use-font-weight-tokens": useFontWeightTokens,
};
diff --git a/tools/lint/stylelint/stylelint-plugin-mozilla/rules/use-font-weight-tokens.mjs b/tools/lint/stylelint/stylelint-plugin-mozilla/rules/use-font-weight-tokens.mjs
@@ -0,0 +1,99 @@
+/* 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 valueParser from "postcss-value-parser";
+import {
+ createTokenNamesArray,
+ createAllowList,
+ createRawValuesObject,
+ getLocalCustomProperties,
+ isValidTokenUsage,
+ namespace,
+} from "../helpers.mjs";
+
+const {
+ utils: { report, ruleMessages, validateOptions },
+} = stylelint;
+
+const ruleName = namespace("use-font-weight-tokens");
+
+const messages = ruleMessages(ruleName, {
+ rejected: value => `${value} should use a font-weight design token.`,
+});
+
+const meta = {
+ url: "https://firefox-source-docs.mozilla.org/code-quality/lint/linters/stylelint-plugin-mozilla/rules/use-font-weight-tokens.html",
+ fixable: true,
+};
+
+const PROPERTY_NAME = "font-weight";
+
+const tokenCSS = createTokenNamesArray([PROPERTY_NAME]);
+
+const ALLOW_LIST = createAllowList();
+
+const tokenFixMap = createRawValuesObject([PROPERTY_NAME]);
+
+const ruleFunction = primaryOption => {
+ return (root, result) => {
+ const validOptions = validateOptions(result, ruleName, {
+ actual: primaryOption,
+ possible: [true],
+ });
+
+ if (!validOptions) {
+ return;
+ }
+
+ const cssCustomProperties = getLocalCustomProperties(root);
+
+ root.walkDecls(declarations => {
+ // ignore properties other than font-weight
+ if (declarations.prop !== PROPERTY_NAME) {
+ return;
+ }
+
+ if (
+ isValidTokenUsage(
+ declarations.value,
+ tokenCSS,
+ cssCustomProperties,
+ ALLOW_LIST
+ )
+ ) {
+ return;
+ }
+
+ report({
+ message: messages.rejected(declarations.value),
+ node: declarations,
+ result,
+ ruleName,
+ fix: () => {
+ const val = valueParser(declarations.value);
+ let hasFixes = false;
+ val.walk(node => {
+ if (node.type === "word") {
+ const token = tokenFixMap[node.value.trim()];
+ if (token) {
+ hasFixes = true;
+ node.value = token;
+ }
+ }
+ });
+ if (hasFixes) {
+ declarations.value = val.toString();
+ }
+ },
+ });
+ });
+ };
+};
+
+ruleFunction.ruleName = ruleName;
+ruleFunction.messages = messages;
+ruleFunction.meta = meta;
+
+export default ruleFunction;
diff --git a/tools/lint/stylelint/stylelint-plugin-mozilla/tests/use-font-weight-tokens.tests.mjs b/tools/lint/stylelint/stylelint-plugin-mozilla/tests/use-font-weight-tokens.tests.mjs
@@ -0,0 +1,183 @@
+/**
+ * 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 useFontWeightTokens from "../rules/use-font-weight-tokens.mjs";
+
+let plugin = stylelint.createPlugin(
+ useFontWeightTokens.ruleName,
+ useFontWeightTokens
+);
+let {
+ ruleName,
+ rule: { messages },
+} = plugin;
+
+testRule({
+ plugins: [plugin],
+ ruleName,
+ config: [true, { tokenType: "brand" }],
+ fix: false,
+ accept: [
+ // allowed token values
+ {
+ code: ".a { font-weight: var(--font-weight); }",
+ description: "Using font-weight token is valid.",
+ },
+ {
+ code: ".a { font-weight: var(--font-weight-bold); }",
+ description: "Using font-weight-bold token is valid.",
+ },
+ {
+ code: ".a { font-weight: var(--button-font-weight); }",
+ description: "Using button-font-weight token is valid.",
+ },
+ {
+ code: ".a { font-weight: var(--heading-font-weight); }",
+ description: "Using heading-font-weight token is valid.",
+ },
+ // allowed CSS values
+ {
+ code: ".a { font-weight: inherit; }",
+ description: "Using inherit is valid.",
+ },
+ {
+ code: ".a { font-weight: initial; }",
+ description: "Using initial is valid.",
+ },
+ {
+ code: ".a { font-weight: unset; }",
+ description: "Using unset is valid.",
+ },
+ {
+ code: ".a { font-weight:var(--my-local, var(--font-weight-bold)); }",
+ description:
+ "Using a custom property with fallback to design token is valid.",
+ },
+ {
+ code: `
+ :root { --custom-token: var(--font-weight-bold); }
+ .a { font-weight: var(--custom-token); }
+ `,
+ description:
+ "Using a custom property with fallback to a design token is valid.",
+ },
+ ],
+
+ reject: [
+ {
+ code: ".a { font-weight: normal; }",
+ message: messages.rejected("normal"),
+ description: "Using normal keyword should use a design token.",
+ },
+ {
+ code: ".a { font-weight: bold; }",
+ message: messages.rejected("bold"),
+ description: "Using bold keyword should use a design token.",
+ },
+ {
+ code: ".a { font-weight: bolder; }",
+ message: messages.rejected("bolder"),
+ description: "Using bolder keyword should use a design token.",
+ },
+ {
+ code: ".a { font-weight: lighter; }",
+ message: messages.rejected("lighter"),
+ description: "Using lighter keyword should use a design token.",
+ },
+ {
+ code: ".a { font-weight: 100; }",
+ message: messages.rejected("100"),
+ description: "Using a numeric value should use a design token.",
+ },
+ {
+ code: ".a { font-weight: 200; }",
+ message: messages.rejected("200"),
+ description: "Using a numeric value should use a design token.",
+ },
+ {
+ code: ".a { font-weight: 300; }",
+ message: messages.rejected("300"),
+ description: "Using a numeric value should use a design token.",
+ },
+ {
+ code: ".a { font-weight: 400; }",
+ message: messages.rejected("400"),
+ description: "Using a numeric value should use a design token.",
+ },
+ {
+ code: ".a { font-weight: 500; }",
+ message: messages.rejected("500"),
+ description: "Using a numeric value should use a design token.",
+ },
+ {
+ code: ".a { font-weight: 600; }",
+ message: messages.rejected("600"),
+ description: "Using a numeric value should use a design token.",
+ },
+ {
+ code: ".a { font-weight: 700; }",
+ message: messages.rejected("700"),
+ description: "Using a numeric value should use a design token.",
+ },
+ {
+ code: ".a { font-weight: 800; }",
+ message: messages.rejected("800"),
+ description: "Using a numeric value should use a design token.",
+ },
+ {
+ code: ".a { font-weight: 900; }",
+ message: messages.rejected("900"),
+ description: "Using a numeric value should use a design token.",
+ },
+ {
+ code: ".a { font-weight: calc(var(--my-local) + 100); }",
+ message: messages.rejected("calc(var(--my-local) + 100)"),
+ description:
+ "Using a calc() with custom variables should use a design token.",
+ },
+ {
+ code: ".a { font-weight: var(--random-token, 600); }",
+ message: messages.rejected("var(--random-token, 600)"),
+ description: "Using a custom property should use a design token.",
+ },
+ {
+ code: `
+ :root { --custom-token: 600; }
+ .a { font-weight: 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.",
+ },
+ ],
+});
+
+// autofix tests
+testRule({
+ plugins: [plugin],
+ ruleName,
+ config: [true, { tokenType: "brand" }],
+ fix: true,
+ reject: [
+ {
+ code: ".a { font-weight: normal; }",
+ fixed: ".a { font-weight: var(--font-weight); }",
+ message: messages.rejected("normal"),
+ description: "Normal keyword should be fixed to use design token.",
+ },
+ {
+ code: ".a { font-weight: 600; }",
+ fixed: ".a { font-weight: var(--font-weight-bold); }",
+ message: messages.rejected("600"),
+ description: "Numeric value should be fixed to use design token.",
+ },
+ ],
+});