Lint

The idea of token linting is similar to any kind of code linting—you can run checks on your tokens to raise errors and warnings based on a number of criteria:

RuleDescription
core/colorspaceEnforce that all colors are declared in a specific colorspace (e.g. sRGB).
core/consistent-namingEnforce a consistent naming style (e.g. camelCase).
core/duplicate-valueEnforce tokens can’t redeclare the same value (excludes aliases).
core/descriptionsEnforce tokens have descriptions.
core/max-gamutEnforce colors are within the specified gamut (e.g. display-p3).
core/required-childrenEnforce token groups have specific children, whether tokens and/or groups.
core/required-modesEnforce certain tokens have specific modes.
core/required-typography-propertiesEnforce typography tokens have required properties (e.g. lineHeight).
a11y/min-contrastEnsure minimum WCAG 2.2 contrast given token pairs.
a11y/min-font-sizeEnsure minimum font size.

Config

To opt into a lint rule, add a lint.rules object in your config. The key is the lint rule name, and the value is a string indicating the severity:

SeverityDescription
"error"Stop token parsing immediately and throw an error.
"warn"Don’t block token parsing, but print an error to the console.
"off"Disable this rule.

Lastly, many rules accept additional options. If passing options, use the [severity, options] tuple format:

import { defineConfig } from "@terrazzo/cli";

export default defineConfig({
  lint: {
    rules: {
      "core/colorspace": "warn",
      "core/consistent-naming": ["error", { format: "kebab-case" }],
      "core/duplicate-values": "off",
    },
  },
});

Rules

core/colorspace

Enforce that all colors are declared in a specific colorspace (e.g. sRGB).

import { defineConfig } from "@terrazzo/cli";

export default defineConfig({
  lint: {
    rules: {
      "core/colorspace": ["error", { colorSpace: "oklab" }],
    },
  },
});
OptionDescription
colorSpaceAny valid CSS Color Module 4 predefined color space, e.g. srgb, display-p3, oklab, etc.
ignoreArray of token globs to ignore, e.g. ('["legacy.*"])

core/consistent-naming

Enforce a consistent naming style (e.g. camelCase).

import { defineConfig } from "@terrazzo/cli";

export default defineConfig({
  lint: {
    rules: {
      "core/consistent-naming": ["error", { format: "kebab-case" }],
    },
  },
});
OptionDescription
formatkebab-case, camelCase, PascalCase, snake_case, SCREAMING_SNAKE_CASE, or a custom validator function.
ignoreArray of token globs to ignore, e.g. ('["legacy.*"])

core/duplicate-value

Enforce tokens can’t redeclare the same value (excludes aliases).

import { defineConfig } from "@terrazzo/cli";

export default defineConfig({
  lint: {
    rules: {
      "core/duplicate-value": "error",
    },
  },
});
OptionDescription
ignoreArray of token globs to ignore, e.g. ('["legacy.*"])

core/descriptions

Enforce tokens have descriptions. Having a description on a group doesn’t count.

import { defineConfig } from "@terrazzo/cli";

export default defineConfig({
  lint: {
    rules: {
      "core/descriptions": "error",
    },
  },
});
OptionDescription
ignoreArray of token globs to ignore, e.g. '["legacy.*"]

core/max-gamut

Enforce colors are within the specified gamut.

import { defineConfig } from "@terrazzo/cli";

export default defineConfig({
  lint: {
    rules: {
      "core/max-gamut": ["error", { format: "srgb" }],
    },
  },
});
OptionDescription
gamutsrgb, p3, or rec2020.
ignoreArray of token globs to ignore, e.g. '["legacy.*"]

core/required-children

Enforce token groups have specific children, whether tokens and/or groups.

A Match consists of a match glob to match against, along with either requiredTokens and/or requiredGroups. Here are some examples:

import { defineConfig } from "@terrazzo/cli";

export default defineConfig({
  lint: {
    rules: {
      "core/required-children": [
        "error",
        {
          matches: [
            // example 1: require all color.base tokens to be a ramp with 100–900 values
            {
              match: ["color.base.*"],
              requiredTokens: ["100", "200", "300", "400", "500", "600", "700", "800", "900"],
            },

            // example 2: require all text.* and bg.* tokens to have specific states
            {
              match: ["text.*", "bg.*"],
              requiredGroups: ["action", "disabled", "error", "warn", "success"],
            },
          ],
        },
      ],
    },
  },
});
OptionDescription
matchesArray of Matches.
matches[n].matchArray of token globs to include, e.g. ["color.*"]
matches[n].requiredTokensArray of strings to match against sub-token IDs, e.g. ["100", "200", …]
matches[n].requiredGroupsArray of strings to match against sub-group IDs, e.g. ["action", "disabled", …]

core/required-modes

Enforce certain tokens have specific modes.

import { defineConfig } from "@terrazzo/cli";

export default defineConfig({
  lint: {
    rules: {
      "core/required-modes": [
        "error",
        {
          matches: [
            {
              match: ["color.*"],
              modes: ["light", "dark"],
            },
          ],
        },
      ],
    },
  },
});
OptionDescription
matchesArray of Matches.
matches[n].matchArray of token globs to include, e.g. ["size.*", "typography.*"]
matches[n].modesArray of strings to match against mode names, e.g. ["mobile", "desktop", …]

core/required-typography-properites

Enforce typography tokens have required properties.

import { defineConfig } from "@terrazzo/cli";

export default defineConfig({
  lint: {
    rules: {
      "core/required-typography-properties": [
        "error",
        {
          properties: [
            "fontFamily",
            "fontSize",
            "fontStyle",
            "fontWeight",
            "letterSpacing",
            "lineHeight",
          ],
        },
      ],
    },
  },
});
OptionDescription
propertiesArray of required properties, e.g. ["fontFamily", "fontSize", "letterSpacing", …]
ignoreArray of token globs to ignore, e.g. '["typography.legacy.*"]

a11y/min-contrast

Enforce colors meet minimum contrast checks for WCAG 2. Rather than test every possible combination in your color system, which would lead to lots of false positives, instead you declare your contrast pairs.

Each pair consists of a foreground and background color to test (note that while WCAG 2 doesn’t distinguish between foreground and background, some other color algorithms do). Optionally, you can set largeText = true if this is for large or bold text (which lessens the contrast requirements a bit).

import { defineConfig } from "@terrazzo/cli";

export default defineConfig({
  lint: {
    rules: {
      "a11y/min-contrast": [
        "error",
        {
          level: "AA",
          pairs: [
            {
              foreground: "color.text-primary",
              background: "color.bg-primary",
            },
            {
              foreground: "color.error-text",
              background: "color.error-bg",
            },
            {
              foreground: "color.action-text",
              background: "color.action-bg",
            },
          ],
        },
      ],
    },
  },
});
OptionDescription
levelAA or AAA, corresponding to the WCAG conformance levels
pairsPairs of tokens to test.
pairs[n].foregroundToken ID of the foreground to test (must be a color token)
pairs[n].backgroundToken ID of the foreground to test (must be a color token)
pairs[n].largeText(Optional) Is this large text?

a11y/min-font-size

Enforce font sizes are no smaller than the given value.

import { defineConfig } from "@terrazzo/cli";

export default defineConfig({
  lint: {
    rules: {
      "a11y/min-font-size": ["error", { minSizeRem: 1 }],
    },
  },
});
OptionDescription
minSizePxMin screen pixel size a font may be.
minSizeRemMin rem size a font may be.
ignoreArray of token globs to ignore, e.g. ["legacy.*"]

Writing your own plugin

Writing your own linter is easy! Terrazzo’s lint API is heavily-inspired by ESLint, but even simpler. See the plugin section on linting to get started.

Written a linter yourself? Add it!