Show contrast ratios in contrast pair tables

This commit is contained in:
Lea Verou
2025-01-16 12:47:06 -05:00
parent fba0b11343
commit df51149d0a
7 changed files with 139 additions and 56 deletions

1
docs/_data/palettes.js Normal file
View File

@@ -0,0 +1 @@
export { default as default } from '../../src/styles/color/palettes.js';

View File

@@ -15,15 +15,24 @@
<tr>
<th>{{ hue | capitalize }}</th>
{% for tint_bg in tints -%}
{% for tint_fg in tints | reverse -%}
{% if (tint_fg - tint_bg) | abs == difference %}
<td>
<div class="color swatch" style="background-color: var(--wa-color-{{ hue }}-{{ tint_bg }}); color: var(--wa-color-{{ hue }}-{{ tint_fg }})">
{{ tint_fg }} on {{ tint_bg }}
</div>
</td>
{% endif %}
{% set color_bg = palettes[paletteId][hue][tint_bg] %}
{% for tint_fg in tints | reverse -%}
{% set color_fg = palettes[paletteId][hue][tint_fg] %}
{% if (tint_fg - tint_bg) | abs == difference %}
<td>
<div class="color swatch" style="background-color: var(--wa-color-{{ hue }}-{{ tint_bg }}); color: var(--wa-color-{{ hue }}-{{ tint_fg }})">
{% set contrast_wcag = '' %}
{% if color_fg and color_bg %}
{% set contrast_wcag = color_bg.contrast(color_fg, 'WCAG21') %}
{% endif %}
{% if contrast_wcag %}
{{ contrast_wcag | number({maximumSignificantDigits: 2}) }}
{% else %}
{{ tint_fg }} on {{ tint_bg }}
{% endif %}
</div>
</td>
{% endif %}
{%- endfor -%}
{%- endfor -%}
</tr>

View File

@@ -61,6 +61,16 @@ A difference of `40` ensures a minimum **3:1** contrast ratio, suitable for larg
{% include "contrast-table.njk" %}
{% markdown %}
This also goes for a difference of `45`:
{% endmarkdown %}
{% set difference = 45 %}
{% include "contrast-table.njk" %}
{% markdown %}
### Level 2
A difference of `50` ensures a minimum **4.5:1** contrast ratio, suitable for normal text (AA) and large text (AAA)
@@ -69,6 +79,15 @@ A difference of `50` ensures a minimum **4.5:1** contrast ratio, suitable for no
{% set difference = 50 %}
{% include "contrast-table.njk" %}
{% markdown %}
This also goes for a difference of `55`:
{% endmarkdown %}
{% set difference = 55 %}
{% include "contrast-table.njk" %}
{% markdown %}
### Level 3
@@ -78,6 +97,15 @@ A difference of `60` ensures a minimum **7:1** contrast ratio, suitable for all
{% set difference = 60 %}
{% include "contrast-table.njk" %}
{% markdown %}
This also goes for a difference of `65`:
{% endmarkdown %}
{% set difference = 65 %}
{% include "contrast-table.njk" %}
{% markdown %}
## How to use this palette

View File

@@ -105,6 +105,23 @@ export function deepValue(obj, key) {
return key.reduce((subObj, property) => subObj?.[property], obj);
}
export function number(value, options) {
if (typeof value !== 'number' && isNaN(value)) {
return value;
}
let lang = options?.lang ?? 'en';
if (options?.lang) {
delete options.lang;
}
if (!options || Object.keys(options).length === 0) {
options = { maximumSignificantDigits: 3 };
}
return Number(value).toLocaleString(lang, options);
}
export function isNumeric(value) {
return typeof value === 'number' || (typeof value === 'string' && !isNaN(value));
}

View File

@@ -54,6 +54,7 @@
"create": "plop --plopfile scripts/plop/plopfile.js",
"test": "web-test-runner --group default",
"test:component": "web-test-runner -- --watch --group",
"test:contrast": "cd src/styles/color && node contrast.test.js",
"test:watch": "web-test-runner --watch --group default",
"prettier": "prettier --check --log-level=warn .",
"prettier:fix": "prettier --write --log-level=warn .",

View File

@@ -1,34 +1,7 @@
// Get a list of all CSS files in repo
import chalk from 'chalk';
import Color from 'colorjs.io';
import fs from 'fs';
import path from 'path';
import { fileURLToPath } from 'url';
const __dirname = fileURLToPath(new URL('.', import.meta.url));
const paletteFiles = fs.readdirSync(__dirname).filter(file => file.endsWith('.css'));
function parse(contents) {
// Regex for each declaration
let regex = /^\s*--wa-color-(?<hue>[a-z]+)-(?<level>[0-9]+):\s*(?<color>[^;]+)\s*;$/gm;
let matches = [...contents.matchAll(regex)];
if (matches.length === 0) {
throw new Error('Cound not extract colors');
}
let ret = {};
for (let match of matches) {
let { hue, level, color } = match.groups;
ret[hue] ??= {};
level = level.replace(/^0+/, ''); // Leading zeroes throw off sorting
ret[hue][level] = color;
}
return ret;
}
import palettes from './palettes.js';
let targetContrasts = {
40: 3,
@@ -38,21 +11,17 @@ let targetContrasts = {
let result = { pass: 0, fail: 0, invalid: 0 };
for (let file of paletteFiles) {
let css = fs.readFileSync(path.join(__dirname, file), 'utf8');
let filePrefix = chalk.dim(`[${file}]`);
let tokens = parse(css);
for (let paletteId in palettes) {
const tokens = palettes[paletteId];
let prefix = chalk.dim(`[${paletteId}]`);
for (let hue in tokens) {
let tints = tokens[hue];
for (let tint = 10; tint <= 50; tint += 10) {
let color;
let color = tints[tint];
try {
color = new Color(tints[tint]);
} catch (e) {
console.error(`[${file}] Invalid color ${hue}-${tint}: ${tints[tint]}`);
if (!(color instanceof Color)) {
result.invalid++;
continue;
}
@@ -64,15 +33,10 @@ for (let file of paletteFiles) {
continue;
}
let color2;
try {
color2 = new Color(tints[tint2]);
} catch (e) {
if (tint2 > 50) {
// If 50, we'll look at it at some point as color1
console.error(`${filePrefix} Invalid color ${hue}-${tint2}: ${tints[tint2]}`);
result.invalid++;
}
let color2 = tints[tint2];
if (!(color2 instanceof Color)) {
result.invalid++;
continue;
}
@@ -84,7 +48,7 @@ for (let file of paletteFiles) {
result.fail++;
console.log(
chalk.red(
`${filePrefix} WCAG 2.1 contrast between ${hue}-${tint} and ${hue}-${tint2} is ${contrast.toLocaleString('en')} < ${targetContrast}`,
`${prefix} WCAG 2.1 contrast between ${hue}-${tint} and ${hue}-${tint2} is ${contrast.toLocaleString('en')} < ${targetContrast}`,
),
);
}

View File

@@ -0,0 +1,63 @@
/**
* Export data on all color tokens from all palettes
*/
// Get a list of all CSS files in repo
import Color from 'colorjs.io';
import fs from 'fs';
import path from 'path';
import { fileURLToPath } from 'url';
const __dirname = fileURLToPath(new URL('.', import.meta.url));
export const paletteFiles = fs.readdirSync(__dirname).filter(file => file.endsWith('.css'));
export const declarationRegex = /^\s*--wa-color-(?<hue>[a-z]+)-(?<level>[0-9]+):\s*(?<color>[^;]+)\s*;$/gm;
function parse(contents, file) {
// Regex for each declaration
const matches = [...contents.matchAll(declarationRegex)];
if (matches.length === 0) {
throw new Error('Cound not extract colors');
}
const ret = {};
for (let match of matches) {
let { hue, level, color } = match.groups;
ret[hue] ??= {};
// Attempt to convert color to Color object, fall back to string if this fails
// This will happen for e.g. colors defined via color-mix()
try {
color = new Color(color);
} catch (e) {
console.warn(`[${file}] Unparseable color ${hue}-${level}: ${color}`);
}
if (level.startsWith('0')) {
// Leading zeroes throw off sorting, add both properties
// NOTE: Ideally one of the two would be added as non-enumerable, but then we cannot access it via 11ty data
ret[hue][level] = color;
// Drop leading zeroes
level = level.replace(/^0+/, '');
}
ret[hue][level] = color;
}
return ret;
}
const palettes = {};
for (let file of paletteFiles) {
let css = fs.readFileSync(path.join(__dirname, file), 'utf8');
let tokens = parse(css, file);
let paletteId = file.replace(/\.css$/, '');
palettes[paletteId] = tokens;
}
export default palettes;