Skip to content

Linting

Token linting is the same as any other type of code linting—you can catch common errors in your tokens during your build such as naming mismatches, duplicate values, and even color contrast checks. Cobalt’s powerful linting capabilities can help catch a multitude of issues in any tokens manifest cheaply and instantly, without complex and expensive testing setups.

You can configure the rules in lint.rules key in tokens.config.js. To run the linter, run:

sh
co lint

The syntax is similar to many linters such as ESLint. First you declare a rule, then whether you want it to throw an error, warn, or to be off completely:

js
/** @type {import("@cobalt-ui/core").Config} */
export default {
  lint: {
    build: { enabled: true }, // set { enabled: "false" } to skip linting on build
    rules: {
      "my-lint-rule": "error", // throw an error for this rule
      "my-lint-rule": "warn", // throw a warning for this rule
      "my-lint-rule": "off", // disable this rule
    },
  },
};

Some rules allow for additional settings, depending on the rule. To set those, pass an array with the 1st item being a severity and the 2nd item being the options:

js
export default {
  lint: {
    build: { enabled: true },
    rules: {
      "my-lint-rule": ["error", { foo: "bar" }], // throw an error AND configure settings only for this rule
    },
  },
};

Not all rules have options. Refer to each rule’s documentation to see what it can accept.

But you

Built-in Rules

The following rules are all available in @cobalt-ui/core by default without any additional plugins.

RuleDefaultDescription
duplicate-values"warn"Enforce no tokens have duplicate values.
naming"off"Enforce all token names follow a specific style (e.g. kebab-case or camelCase)
required-children"off"Enforces matching groups have required children.
required-modes"off"Enforces matching tokens have required modes.
color/format"off"Enforce color tokens are declared in a certain format (e.g. oklch).
color/gamut"warn"Enforce color tokens are displayable within the specified gamut (srgb, p3, or rec2020).
typography/required-properties"off"Enforce typography tokens have specific properties (e.g. fontWeight).

duplicate-values

Enforce no tokens have duplicate values.

js
{
  "duplicate-values": ["error", { ignore: ["color.semantic.*"] }],
}
OptionTypeDescription
ignorestring[](Optional) Token IDs to ignore. Supports globs (*).
❌ Failures
json
{
  "color": {
    "blue": {
      "100": { "$type": "color", "$value": "#3c3c43" },
      "200": { "$type": "color", "$value": "#3c3c43" }
    }
  }
}
✅ OK

Aliases are OK:

json
{
  "color": {
    "blue": {
      "100": { "$type": "color", "$value": "#3c3c43" },
      "200": { "$type": "color", "$value": "{color.blue.100}" }
    }
  }
}

Values that are equivalent but formatted differently aren’t violations:

json
{
  "color": {
    "rgb": { "$type": "color", "$value": "#3c3c43" },
    "hsl": { "$type": "color", "$value": "hsl(240, 5.5%, 24.9%)" }
  }
}

naming

Require token IDs to match a consistent format.

js
{
  naming: ["error", { format: "kebab-case" }],
}

Alternately, provide your own function that returns an error string on failure:

js
{
  naming: [
    "error",
    {
      format: (tokenID) => (tokenID.includes("bad-word") ? "No bad words allowed!" : undefined),
    },
  ],
}
OptionTypeDescription
formatstring | FunctionEnforce kebab-case, PascalCase, camelCase, snake_case, SCREAMING_SNAKE_CASE, or provide your own function.
ignorestring[](Optional) Token IDs to ignore. Supports globs (*).
❌ Failures

actionText should be action-text:

json
{
  "actionText": { "$type": "color", "$value": "#3c3c43" }
}
✅ OK
json
{
  "action-text": { "$type": "color", "$value": "#3c3c43" }
}

required-children

Require specific groups to have specific children.

OptionTypeDescription
matchesMatch[]Array of Matches. A Match has a match and requiredTokens and/or requiredGroups. All are string[]s. Only match supports globs (*).
js
{
  "required-children": [
    "error", // or "warn"
    {
      matches: [
        {
          match: ["color.base.*"],
          requiredTokens: ["100", "200", "300"],
        },
        {
          match: ["color.semantic.*"],
          requiredGroups: ["action", "error"],
        },
      ],
    },
  ],
}
❌ Failures

color.blue missing required child token 300:

json
{
  "color": {
    "base": {
      "blue": {
        "100": { "$type": "color", "$value": "#fbfdff" },
        "200": { "$type": "color", "$value": "#f4faff" }
      }
    }
  }
}

color.semantic.* missing required group error:

json
{
  "color": {
    "semantic": {
      "action": {
        "text": { "$type": "color", "$value": "#5eb1ef" },
        "bg": { "$type": "color", "$value": "#fbfdff" }
      }
    }
  }
}
✅ OK

100, 200, and 300 are present for the entire color.* group:

json
{
  "color": {
    "blue": {
      "100": { "$type": "color", "$value": "#fbfdff" },
      "200": { "$type": "color", "$value": "#f4faff" },
      "300": { "$type": "color", "$value": "#e6f4fe" }
    }
  }
}

color.semantic.* has both action and error groups:

json
{
  "color": {
    "semantic": {
      "action": {
        "text": { "$type": "color", "$value": "#5eb1ef" },
        "bg": { "$type": "color", "$value": "#fbfdff" }
      },
      "error": {
        "text": { "$type": "color", "$value": "#eb8e90" },
        "bg": { "$type": "color", "$value": "#fff7f7" }
      }
    }
  }
}

required-modes

Require tokens to have specific modes. Provide a key–value list where the key is a token or group name (supports globs), and the value is an array of strings with required modes.

OptionTypeDescription
matchesMatch[]Array of Matches. A Match has a match and requiredModes. All are string[]s. Only match supports globs (*).

TIP

This replaces the older $extensions.requiredModes property that lives in tokens.json. This is more flexible, and doesn’t clutter up your tokens.

js
{
  "required-modes": [
    "error", // or "warn"
    {
      matches: [
        {
          match: ["typography.*"],
          requiredModes: ["mobile", "desktop"],
        },
      ],
    },
  ],
}
❌ Failures

typography.size.body missing required mode "mobile"

json
{
  "typography": {
    "size": {
      "body": {
        "$type": "dimension",
        "$value": "16px",
        "$extensions": {
          "mode": {
            "desktop": "16px"
          }
        }
      }
    }
  }
}
✅ OK

typography.size.body has all required modes "mobile" and "desktop"

json
{
  "typography": {
    "size": {
      "body": {
        "$type": "dimension",
        "$value": "16px",
        "$extensions": {
          "mode": {
            "mobile": "16px",
            "desktop": "16px"
          }
        }
      }
    }
  }
}

color/format

WARNING

color/format will have breaking changes when the DTCG spec changes its color format (discussion). If using this lint rule, expect changes in the future.

Require color tokens to match a specific format (only CSS Color Module 4 colorspaces supported).

OptionTypeDescription
formatstringSpecify any of the following: hex, srgb, a98-rgb, display-p3, hsl, hsv, hwb, lab, lch, oklab, oklch, prophoto-rgb, rec2020, srgb-linear, xyz50, or xyz65
ignorestring[](Optional) Token IDs to ignore. Supports globs (*).
js
{
  "color/format": ["error", { format: "oklch" }],
}
❌ Failures

color.base.purple doesn’t match mode oklch:

json
{
  "color": {
    "base": {
      "purple": { "$type": "color", "$value": "#ab4aba" }
    }
  }
}
✅ OK

color.base.purple matches oklch format:

json
{
  "color": {
    "base": {
      "purple": { "$type": "color", "$value": "oklch(57.87% 0.188 322.11)" }
    }
  }
}

color/gamut

Enforces colors are within srgb gamut (smallest), p3 (medium; contains all of sRGB), or rec2020 (largest; contains all of sRGB + P3).

Require color tokens to match a specific format (only CSS Color Module 4 colorspaces supported).

OptionTypeDescription
gamutstringsrgb, p3, or rec2020.
ignorestring[](Optional) Token IDs to ignore. Supports globs (*).
js
{
  "color/gamut": ["error", { gamut: "srgb" }],
}
❌ Failures

color.base.teal is outside the sRGB range, which means it won’t display the same on older monitors:

json
{
  "color": {
    "base": {
      "teal": { "$type": "color", "$value": "oklch(87.65% 0.276 173.65)" }
    }
  }
}
✅ OK

color.base.teal is safely within the sRGB range:

json
{
  "color": {
    "base": {
      "teal": { "$type": "color", "$value": "oklch(87.59% 0.167 173.65)" }
    }
  }
}

typography/required-properties

Enforces typography tokens have required properties. This is especially helpful when using variable fonts.

OptionTypeDescription
propertiesstring[]List of properties all typography tokens should have.
ignorestring[](Optional) Token IDs to ignore. Supports globs (*).
js
{
  "typography/required-properties": ["error", { properties: ["fontSize", "fontFamily", "fontStyle", "fontWeight"] }],
}
❌ Failures

typography.base.body missing fontStyle:

json
{
  "typography": {
    "base": {
      "body": {
        "$type": "typography",
        "$value": {
          "fontSize": "14px",
          "fontFamily": ["Helvetica"],
          "fontWeight": 400
        }
      }
    }
  }
}
✅ OK

typography.base.body has all required properties:

json
{
  "typography": {
    "base": {
      "body": {
        "$type": "typography",
        "$value": {
          "fontSize": "14px",
          "fontFamily": ["Helvetica"],
          "fontWeight": 400,
          "fontStyle": "normal"
        }
      }
    }
  }
}

a11y Rules

The a11y plugin can handle color contrast checks for WCAG2.

RuleDefaultDescription
a11y/contrast"off"Run WCAG2 contrast checks on all of your tokens (you have to manually specify the pairs, but it will work on any tokens in tokens.json)
sh
npm i -D @cobalt-ui/lint-a11y
js
import a11y from "@cobalt-ui/lint-a11y";

/** @type {import("@cobalt-ui/core").Config} */
export default {
  plugins: [a11y()],
  lint: {
    rules: {
      "a11y/contrast": [
        "error", // or "warn"
        {
          checks: [
            {
              tokens: {
                foreground: "color.semantic.text",
                background: "color.semantic.bg",
                typography: "typography.body",
                modes: ["light", "dark"],
              },
              wcag2: "AAA",
            },
          ],
        },
      ],
    },
  },
};

View docs

Custom linters

You can also build your own linter by creating your own plugin and giving it a lint step. It’s easier than you might think!

Have you built a plugin you’d like to add here? Suggest it!

Adding to CI

Linting can be added to your project’s package.json by adding the co lint command:

json
{
  "scripts": {
    "lint:tokens": "co lint"
  }
}