# core/stylelint/custom-property-unit-mismatch.stylelint.rule.ts

> 75 lines of code and 12 definitions.

Tree: GovLab Quality
Language: typescript
Canonical: https://banes-lab.com/anatomy/quality#file-quality-core-stylelint-custom-property-unit-mismatch-stylelint-rule-ts
Source text: https://banes-lab.com/source/quality/core/stylelint/custom-property-unit-mismatch.stylelint.rule.ts.txt

Listed in [core/stylelint](https://banes-lab.com/api/source/quality/core/stylelint.md), after [core/stylelint/custom-property-order.stylelint.rule.ts](https://banes-lab.com/source/quality/core/stylelint/custom-property-order.stylelint.rule.ts.md) and before [core/stylelint/no-css-magic-number.stylelint.rule.ts](https://banes-lab.com/source/quality/core/stylelint/no-css-magic-number.stylelint.rule.ts.md).

## Definitions

- `{ createPlugin, utils }` (lexical_declaration, line 8)
- `ruleName` (lexical_declaration, line 9)
- `ruleId` (lexical_declaration, line 10)
- `RULE_META` (lexical_declaration, line 12, exported)
- `REM_PREFIXES` (lexical_declaration, line 22)
- `PX_PREFIXES` (lexical_declaration, line 35)
- `messages` (lexical_declaration, line 42)
- `rule` (lexical_declaration, line 55)
- `name` (lexical_declaration, line 62)
- `units` (lexical_declaration, line 66)
- `isPx` (lexical_declaration, line 67)
- `isRem` (lexical_declaration, line 68)

## Contained in

- [core/stylelint](https://banes-lab.com/anatomy/quality/folder-quality-core-stylelint.md)

## Source

```typescript
import type { Declaration, Root } from "postcss";
import stylelint, { type PostcssResult, type Rule } from "stylelint";
import type { GovlabStylelintMeta } from "#types/stylelint.types";
import { customPropertyStartsWith } from "#core/predicates/css.predicate";
import { valueUnits } from "#core/parsers/css.parser";
import { withRuleId } from "#core/formatters/stylelint.formatter";

const { createPlugin, utils } = stylelint;
const ruleName = "govlab/custom-property-unit-mismatch";
const ruleId = "custom_property_unit_mismatch";

export const RULE_META: GovlabStylelintMeta = {
    meta: {
        canonical: ["design-tokens"],
        description: "Require custom-property values to use the unit their name prefix mandates (rem vs px)",
        fixable: false,
    },
    ruleId,
    ruleName,
};

const REM_PREFIXES = [
    "--font-",
    "--width-",
    "--height-",
    "--size-",
    "--touch-target-",
    "--nav-width",
    "--nav-height",
    "--header-width",
    "--header-height",
    "--space-",
    "--radius-",
];
const PX_PREFIXES: readonly { prefix: string; excludes: readonly string[] }[] = [
    { excludes: ["neutral"], prefix: "--border-" },
    { excludes: [], prefix: "--bp-" },
    { excludes: [], prefix: "--cq-" },
    { excludes: [], prefix: "--transform-hover-" },
];

const messages = utils.ruleMessages(ruleName, {
    pxExpected: (name: string): string =>
        withRuleId(
            `Custom property '${name}' matches a px-required pattern but uses rem. Convert the value to px — borders and breakpoints are fixed-pixel by contract.`,
            ruleId,
        ),
    remExpected: (name: string): string =>
        withRuleId(
            `Custom property '${name}' matches a rem-required pattern but uses px. Convert the value to rem (÷16) so it scales with the root font size.`,
            ruleId,
        ),
});

const rule: Rule = Object.assign(
    (primary: unknown) =>
        (root: Root, result: PostcssResult): void => {
            if (primary !== true) {
                return;
            }
            root.walkDecls((decl: Declaration) => {
                const name = decl.prop;
                if (!name.startsWith("--")) {
                    return;
                }
                const units = valueUnits(decl.value);
                const isPx = units.some((token) => token.unit === "px");
                const isRem = units.some((token) => token.unit === "rem");
                if (REM_PREFIXES.some((prefix) => customPropertyStartsWith(name, prefix)) && isPx) {
                    utils.report({ message: messages.remExpected(name), node: decl, result, ruleName });
                    return;
                }
                if (PX_PREFIXES.some((p) => customPropertyStartsWith(name, p.prefix, p.excludes)) && isRem) {
                    utils.report({ message: messages.pxExpected(name), node: decl, result, ruleName });
                }
            });
        },
    { messages, ruleName },
);

export default createPlugin(ruleName, rule);
```
