Skip to content

A11y Integration (beta)

Lint your token schema for a11y errors. Can check your color + typography tokens for contrast.

Install the plugin:

sh
npm i -D @cobalt-ui/lint-a11y

Then add to your tokens.config.js file:

js
// tokens.config.js
import a11y from "@cobalt-ui/lint-a11y";

/** @type {import("@cobalt-ui/core").Config} */
export default {
  tokens: "./tokens.json",
  outDir: "./tokens/",
  plugins: [a11y()],
  lint: {
    // checks
  },
};
RuleDefaultDescription
a11y/contrast"off"Run WCAG2 and APCA contrast checks on all of your tokens (you have to manually specify the pairs, but it will work on any tokens in tokens.json)

a11y/contrast

The contrast check asserts your token combinations the latest WCAG 2.1 and APCA (WCAG 3 proposal) formulae. Add an array of checks to test:

js
import a11y from "@cobalt-ui/lint-a11y";

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

Check options

Within each check group, specify:

NameTypeDescription
tokensobjectA group of tokens to test together.
tokens.foregroundstringThe ID of the foreground color.
tokens.backgroundstringThe ID of the background color.
tokens.typographystring(optional) The ID of a typography stack
tokens.modesstring[](optional) Any modes you’d like to test
wcag2string | number | falseSpecify "AA" or "AAA" compliance (or a minimum contrast), or false to disable (default: "AA"). See WCAG 2
apca"silver" | "silver-nonbody" | number | falseSpecify Silver compliance or a specific Lc number (default: false). See APCA.

WCAG 2

The WCAG 2 contrast formula is represented by the wcag2 setting and accepts either a string, number, or false:

ts
{
  checks: [
    {
      tokens: { /* … */ },
      wcag2: "AA"; // "AAA" | "AA" | number | false
    },
  ],
}

The WCAG 2 standard is the most common contrast standard, so "AA" level is enforced by default by this plugin.

Add a typography token value to automatically figure out if you’re using large text (which lowers the minimum contrast requirement).

APCA (beta)

The APCA contrast algorithm is still in beta, but is a likely candidate for the upcoming WCAG 3 contrast algorithm. Like WCAG 2 there is a “contrast ratio” under-the-hood, but it’s referred to as “perceptual lightness contrast,” or Lc. It’s a number ranging from 0100, and like WCAG 2 your target number depends on a few factors. The rough equivalence is:

LcWCAG 2 Contrast Ratio
907:1
754.5:1
603:1

But the math isn’t as simple as WCAG 2; you’ll have to do some work to determine what your target Lc is for any color/font size/font weight trio. And so, APCA specifies “Bronze” and “Silver” levels of compliance (“Gold” isn’t outlined yet). Cobalt can determine Silver compliance automatically for you, but you’ll need to manually manage Bronze compliance for now (explained below).

ts
{
  checks: [
    {
      tokens: { /* … */ },
      apca: "silver"; // "silver" | "silver-nonbody" | number | false
    },
  ],
}
SettingDescription
"bronze"(not supported in Cobalt; set Lc manually following “Bronze” guide)
"silver"Enforce Silver-level compliance for body text. Requires typography token.
"silver-nonbody"Silver-level compliance for non-body text (less strict). Requires typography token.
"gold"(not supported in APCA yet)
number⚠️ Advanced users: specify Lc.
falseDisable APCA.

WARNING

APCA is still a draft, and not part of WCAG 3. But APCA is well-researched and widely-regarded as an improvement over WCAG 2. Compliance with APCA doesn’t guarantee compliance with WCAG 3 when it releases.

Silver (auto) mode vs Number (manual) mode

The same basic principle applies to both WCAG 2 and APCA: contrast is a triangle of color–font size–font weight. Lc only refers to color contrast—you then have to figure out the other two “points” of the triangle—font size and font weight—to complete the picture. It makes sense when you think about it: if you only factored in color contrast, a 2px-tall font would “pass” if the color contrast were good enough.

However, trying to reduce typography into pure math turns into a can of worms quickly, so APCA makes some concessions in letting you, the designer, have more say over what your target Lc is. More flexibility comes at the cost of more manual intervention. To navigate that, Cobalt has 2 “flavors” of APCA support: Silver compliance (auto), and Number (manual).

Silver (auto) mode

Silver mode requires a typography token; it will err without that. But with all that provided, Cobalt does the work for you. Set apca to "silver" (body text) or "silver-nonbody" (non-body text; less strict). If this works for you, yay.

Number (manual) mode

As mentioned earlier, APCA gives you more autonomy over declaring what your “body” font style is, but at the tradeoff of no concrete numbers. In Number (manual) mode, you’ll have to specify Lc manually. Using manual mode you can still pass Bronze and Silver levels, but all Cobalt can do is catch regressions; the compliance part is up to you. Here’s the guide copied directly from APCA’s (all rights ©️ Myndex):

LcDescription
90Preferred level for fluent text and columns of body text with a font no smaller than 18px/weight 300 or 14px/weight 400 (normal), or non-body text with a font no smaller than 12px. Also a recommended minimum for extremely thin fonts with a minimum of 24px at weight 200. Lc 90 is a suggested maximum for very large and bold fonts (greater than 36px bold), and large areas of color.
75The minimum level for columns of body text with a font no smaller than 24px/300 weight, 18px/400, 16px/500 and 14px/700. This level may be used with non-body text with a font no smaller than 15px/400. Also, Lc 75 should be considered a minimum for larger for any larger text where readability is important.
60The minimum level recommended for content text that is not body, column, or block text. In other words, text you want people to read. The minimums: no smaller than 48px/200, 36px/300, 24px normal weight (400), 21px/500, 18px/600, 16px/700 (bold). These values based on the reference font Helvetica. To use these sizes as body text, add Lc 15 to the minimum contrast.
45The minimum for larger, heavier text (36px normal weight or 24px bold) such as headlines, and large text that should be fluently readabile but is not body text. This is also the minimum for pictograms with fine details, or smaller outline icons, , no less than 4px in its smallest dimension.
30The absolute minimum for any text not listed above, which means non-content text considered as "spot readable". This includes placeholder text and disabled element text, and some non-content like a copyright bug. This is also the minimum for large/solid semantic & understandable non-text elements such as "mostly solid" icons or pictograms, no less than 10px in its smallest dimension.
15The absolute minimum for any non-text that needs to be discernible and differentiable, but does not apply to semantic non-text such as icons, and is no less than 15px in its smallest dimention. This may include dividers, and in some cases large buttons or thick focus visible outlines, but does not include fine details which have a higher minimum. Designers should treat anything below this level as invisible, as it will not be visible for many users. This minimum level should be avoided for any items important to the use, understanding, or interaction of the site.
Note on how APCA handles typography

APCA’s typography tables are based off Helvetica. Most people aren’t using Helvetica as their brand font, so APCA allows some wiggle room in interpreting your actual contrast numbers (see Notes on Font Size & Weight). Read the guide and see if your actual Lc is different than what Cobalt is reporting, and adjust by-hand (apca: 59).

Bridge PCA

Bridge PCA is also a creation of Myndex (same as APCA) and is meant to help “bridge” the gap between WCAG 2 and APCA.

Bridge PCA isn’t supported yet but will be in an upcoming release.

Others

Are there other checks that you’d like to see here? Suggest one!