Skip to content

Commit a3a298b

Browse files
authored
contrast-color: drop the max keyword (#1588)
1 parent b3b90fe commit a3a298b

28 files changed

Lines changed: 134 additions & 698 deletions

packages/css-color-parser/CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
# Changes to CSS Color Parser
22

3+
### Unreleased (patch)
4+
5+
- Drop the `max` keyword for `contrast-color( <color> )`
6+
37
### 3.0.8
48

59
_February 23, 2025_

packages/css-color-parser/dist/index.cjs

Lines changed: 1 addition & 1 deletion
Large diffs are not rendered by default.

packages/css-color-parser/dist/index.mjs

Lines changed: 1 addition & 1 deletion
Large diffs are not rendered by default.

packages/css-color-parser/src/functions/contrast-color.ts

Lines changed: 2 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -3,15 +3,12 @@ import type { ColorParser } from '../color-parser';
33
import type { FunctionNode } from '@csstools/css-parser-algorithms';
44
import { ColorNotation } from '../color-notation';
55
import { SyntaxFlag, colorData_to_XYZ_D50, convertNaNToZero } from '../color-data';
6-
import { isCommentNode, isTokenNode, isWhitespaceNode } from '@csstools/css-parser-algorithms';
6+
import { isCommentNode, isWhitespaceNode } from '@csstools/css-parser-algorithms';
77
import { XYZ_D50_to_sRGB_Gamut } from '../gamut-mapping/srgb';
8-
import { isTokenIdent } from '@csstools/css-tokenizer';
9-
import { toLowerCaseAZ } from '../util/to-lower-case-a-z';
108
import { contrast_ratio_wcag_2_1 } from '@csstools/color-helpers';
119

1210
export function contrastColor(colorMixNode: FunctionNode, colorParser: ColorParser): ColorData | false {
1311
let backgroundColorData: ColorData | false = false;
14-
let maxKeyword: boolean = false;
1512

1613
for (let i = 0; i < colorMixNode.value.length; i++) {
1714
const node = colorMixNode.value[i];
@@ -26,21 +23,10 @@ export function contrastColor(colorMixNode: FunctionNode, colorParser: ColorPars
2623
}
2724
}
2825

29-
if (backgroundColorData && !maxKeyword) {
30-
if (
31-
isTokenNode(node) &&
32-
isTokenIdent(node.value) &&
33-
toLowerCaseAZ(node.value[4].value) === 'max'
34-
) {
35-
maxKeyword = true;
36-
continue;
37-
}
38-
}
39-
4026
return false;
4127
}
4228

43-
if (!backgroundColorData || !maxKeyword) {
29+
if (!backgroundColorData) {
4430
return false;
4531
}
4632

packages/css-color-parser/test/basic/contrast-color-function.mjs

Lines changed: 29 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -4,34 +4,34 @@ import { parse } from '../util/parse.mjs';
44
import { serialize_sRGB_data } from '../util/serialize.mjs';
55

66
const tests = [
7-
['contrast-color( black max )', 'rgb(255, 255, 255)'],
8-
['contrast-color(#333/* */max/* */)', 'rgb(255, 255, 255)'],
9-
['contrast-color(grey max)', 'rgb(0, 0, 0)'],
10-
['contrast-color(#ccc max)', 'rgb(0, 0, 0)'],
11-
['contrast-color(white max)', 'rgb(0, 0, 0)'],
12-
['contrast-color(#1234b0 max)', 'rgb(255, 255, 255)'],
13-
['contrast-color(#b012a0 max)', 'rgb(255, 255, 255)'],
14-
15-
['contrast-color(rgb(0 0 0) max)', 'rgb(255, 255, 255)'],
16-
['contrast-color(color(srgb 0 0 0) max)', 'rgb(255, 255, 255)'],
17-
['contrast-color(color(display-p3 0 0 0) max)', 'rgb(255, 255, 255)'],
18-
['contrast-color(rgb(255 255 255) max)', 'rgb(0, 0, 0)'],
19-
['contrast-color(color(srgb 1 1 1) max)', 'rgb(0, 0, 0)'],
20-
['contrast-color(color(display-p3 1 1 1) max)', 'rgb(0, 0, 0)'],
21-
22-
['contrast-color(rgb(0 0 0 / 0) max)', 'rgb(255, 255, 255)'],
23-
['contrast-color(rgb(0 0 0 / 0.5) max)', 'rgb(255, 255, 255)'],
24-
['contrast-color(rgb(255 255 255 / 0) max)', 'rgb(0, 0, 0)'],
25-
['contrast-color(rgb(255 255 255 / 0.5) max)', 'rgb(0, 0, 0)'],
26-
27-
['contrast-color(contrast-color(#b012a0 max) max)', 'rgb(0, 0, 0)'],
28-
29-
['contrast-color(#3b9595 max)', 'rgb(0, 0, 0)'],
30-
['contrast-color(contrast-color(contrast-color(#3b9595 max) max) max)', 'rgb(0, 0, 0)'],
7+
['contrast-color( black )', 'rgb(255, 255, 255)'],
8+
['contrast-color(#333/* */)', 'rgb(255, 255, 255)'],
9+
['contrast-color(grey)', 'rgb(0, 0, 0)'],
10+
['contrast-color(#ccc)', 'rgb(0, 0, 0)'],
11+
['contrast-color(white)', 'rgb(0, 0, 0)'],
12+
['contrast-color(#1234b0)', 'rgb(255, 255, 255)'],
13+
['contrast-color(#b012a0)', 'rgb(255, 255, 255)'],
14+
15+
['contrast-color(rgb(0 0 0))', 'rgb(255, 255, 255)'],
16+
['contrast-color(color(srgb 0 0 0))', 'rgb(255, 255, 255)'],
17+
['contrast-color(color(display-p3 0 0 0))', 'rgb(255, 255, 255)'],
18+
['contrast-color(rgb(255 255 255))', 'rgb(0, 0, 0)'],
19+
['contrast-color(color(srgb 1 1 1))', 'rgb(0, 0, 0)'],
20+
['contrast-color(color(display-p3 1 1 1))', 'rgb(0, 0, 0)'],
21+
22+
['contrast-color(rgb(0 0 0 / 0))', 'rgb(255, 255, 255)'],
23+
['contrast-color(rgb(0 0 0 / 0.5))', 'rgb(255, 255, 255)'],
24+
['contrast-color(rgb(255 255 255 / 0))', 'rgb(0, 0, 0)'],
25+
['contrast-color(rgb(255 255 255 / 0.5))', 'rgb(0, 0, 0)'],
26+
27+
['contrast-color(contrast-color(#b012a0))', 'rgb(0, 0, 0)'],
28+
29+
['contrast-color(#3b9595)', 'rgb(0, 0, 0)'],
30+
['contrast-color(contrast-color(contrast-color(#3b9595)))', 'rgb(0, 0, 0)'],
3131

3232
// ignore
33-
['contrast-color( black )', ''],
34-
['contrast-color( black min )', ''],
33+
['contrast-color( black max)', ''],
34+
['contrast-color( black min)', ''],
3535
];
3636

3737
for (const test of tests) {
@@ -49,9 +49,9 @@ for (const test of tests) {
4949

5050
{
5151
[
52-
'contrast-color(black max)',
53-
'color-mix(in srgb, contrast-color(black max), contrast-color(white max))',
54-
'rgb(from contrast-color(black max) r g b)',
52+
'contrast-color(black)',
53+
'color-mix(in srgb, contrast-color(black), contrast-color(white))',
54+
'rgb(from contrast-color(black) r g b)',
5555
].forEach((testCase) => {
5656
assert.ok(
5757
color(parse(testCase)).syntaxFlags.has('experimental'),

plugins/postcss-contrast-color-function/CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
# Changes to PostCSS Contrast Color Function
22

3+
### Unreleased (patch)
4+
5+
- Drop the `max` keyword for `contrast-color( <color> )`
6+
37
### 2.0.8
48

59
_February 23, 2025_

plugins/postcss-contrast-color-function/README.md

Lines changed: 5 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -9,32 +9,15 @@ npm install @csstools/postcss-contrast-color-function --save-dev
99
[PostCSS Contrast Color Function] lets you dynamically specify a text color with adequate contrast following the [CSS Color 5 Specification].
1010

1111
```css
12-
.dynamic {
12+
.color {
1313
color: contrast-color(oklch(82% 0.2 330));
1414
}
1515

16-
.max {
17-
color: contrast-color(oklch(30% 0.2 79) max);
18-
}
19-
2016
/* becomes */
2117

22-
.dynamic {
23-
color: color(display-p3 0.15433 0 0.15992);
24-
color: contrast-color(oklch(82% 0.2 330));
25-
}@supports not (color: contrast-color(red max)) {@media (prefers-contrast: more) {.dynamic {
18+
.color {
2619
color: rgb(0, 0, 0);
27-
}
28-
}
29-
}@supports not (color: contrast-color(red max)) {@media (prefers-contrast: less) {.dynamic {
30-
color: color(display-p3 0.2925 0 0.30177);
31-
}
32-
}
33-
}
34-
35-
.max {
36-
color: rgb(255, 255, 255);
37-
color: contrast-color(oklch(30% 0.2 79) max);
20+
color: contrast-color(oklch(82% 0.2 330));
3821
}
3922
```
4023

@@ -80,29 +63,15 @@ postcssContrastColorFunction({ preserve: false })
8063
```
8164

8265
```css
83-
.dynamic {
66+
.color {
8467
color: contrast-color(oklch(82% 0.2 330));
8568
}
8669

87-
.max {
88-
color: contrast-color(oklch(30% 0.2 79) max);
89-
}
90-
9170
/* becomes */
9271

93-
.dynamic {
94-
color: color(display-p3 0.15433 0 0.15992);
95-
}@media (prefers-contrast: more) {.dynamic {
72+
.color {
9673
color: rgb(0, 0, 0);
9774
}
98-
}@media (prefers-contrast: less) {.dynamic {
99-
color: color(display-p3 0.2925 0 0.30177);
100-
}
101-
}
102-
103-
.max {
104-
color: rgb(255, 255, 255);
105-
}
10675
```
10776

10877
[cli-url]: https://github.com/csstools/postcss-plugins/actions/workflows/test.yml?query=workflow/test
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
"use strict";var e=require("@csstools/postcss-progressive-custom-properties"),o=require("@csstools/utilities"),r=require("@csstools/css-tokenizer"),s=require("@csstools/css-parser-algorithms"),t=require("@csstools/css-color-parser"),n=require("@csstools/color-helpers");const a=/^contrast-color$/i;function parseContrastColor(e){if(!s.isFunctionNode(e)||!a.test(e.getName()))return!1;const o=e.value.filter((e=>!s.isWhitespaceNode(e)&&!s.isCommentNode(e)));if(o.length>2)return!1;const t=o[0],n=o[1];return!!t&&(n?!(!s.isTokenNode(n)||!r.isTokenIdent(n.value)||"max"!==n.value[4].value.toLowerCase())&&[t,"max"]:[t])}var c;!function(e){e[e.MORE=0]="MORE",e[e.LESS=1]="LESS",e[e.NO_PREFERENCE=2]="NO_PREFERENCE"}(c||(c={}));const l=/\bcontrast-color\(/i;function transformContrastColor(e,o,a=0){const i=s.replaceComponentValues(s.parseCommaSeparatedListOfComponentValues(r.tokenize({css:e})),(e=>{const a=parseContrastColor(e);if(!a)return;const[l,i]=a;if("max"===i){const o=t.color(e);if(!o)return;return t.serializeRGB(o,!0)}if(i)return;const u=t.color(new s.FunctionNode([r.TokenType.Function,"contrast-color(",-1,-1,{value:"contrast-color"}],[r.TokenType.CloseParen,")",-1,-1,void 0],[l,new s.TokenNode([r.TokenType.Ident,"max",-1,-1,{value:"max"}])]));if(!u)return;if(o===c.MORE)return t.serializeRGB(u,!0);const p=t.color(l);if(!p)return;let f=0;const m=t.color(t.serializeRGB(p,!0));if(!m)return;{const e=n.contrast_ratio_wcag_2_1(m.channels,[0,0,0]),r=n.contrast_ratio_wcag_2_1(m.channels,[1,1,1]);f=o===c.LESS?e>=r?.3:.9:e>=r?.2:.95}const v=t.color(new s.FunctionNode([r.TokenType.Function,"oklch(",-1,-1,{value:"oklch"}],[r.TokenType.CloseParen,")",-1,-1,void 0],[new s.TokenNode([r.TokenType.Ident,"from",-1,-1,{value:"from"}]),l,new s.TokenNode([r.TokenType.Number,f.toString(),-1,-1,{value:f,type:r.NumberType.Number}]),new s.TokenNode([r.TokenType.Ident,"c",-1,-1,{value:"c"}]),new s.TokenNode([r.TokenType.Ident,"h",-1,-1,{value:"h"}])]));if(!v)return;const d=t.color(t.serializeRGB(v,!0));if(!d)return;return n.contrast_ratio_wcag_2_1(m.channels,d.channels)<4.5?t.serializeRGB(u,!0):t.serializeP3(v,!0)})),u=s.stringify(i);return u===e?e:a>10?u:l.test(u)?transformContrastColor(u,o,a+1):u}const basePlugin=e=>({postcssPlugin:"postcss-contrast-color-function",prepare:()=>({postcssPlugin:"postcss-contrast-color-function",Declaration(r,{atRule:s}){const t=r.parent;if(!t)return;if(!l.test(r.value))return;if(o.hasFallback(r))return;if(o.hasSupportsAtRuleAncestor(r,l))return;const n=transformContrastColor(r.value,c.NO_PREFERENCE);if(n===r.value)return;const a=transformContrastColor(r.value,c.LESS);if(a===r.value)return;const i=transformContrastColor(r.value,c.MORE);if(i!==r.value){if(r.cloneBefore({value:n}),n!==a){const o=t.clone();o.removeAll(),o.append(r.clone({value:a}));const n=s({name:"media",params:"(prefers-contrast: less)",source:t.source});if(n.append(o),e?.preserve){const e=s({name:"supports",params:"not (color: contrast-color(red max))",source:t.source});e.append(n),t.after(e)}else t.after(n)}if(n!==i){const o=t.clone();o.removeAll(),o.append(r.clone({value:i}));const n=s({name:"media",params:"(prefers-contrast: more)",source:t.source});if(n.append(o),e?.preserve){const e=s({name:"supports",params:"not (color: contrast-color(red max))",source:t.source});e.append(n),t.after(e)}else t.after(n)}e?.preserve||r.remove()}}})});basePlugin.postcss=!0;const postcssPlugin=o=>{const r=Object.assign({enableProgressiveCustomProperties:!0,preserve:!0},o);return r.enableProgressiveCustomProperties&&r.preserve?{postcssPlugin:"postcss-contrast-color-function",plugins:[e(),basePlugin(r)]}:basePlugin(r)};postcssPlugin.postcss=!0,module.exports=postcssPlugin;
1+
"use strict";var s=require("@csstools/postcss-progressive-custom-properties"),e=require("@csstools/utilities"),o=require("@csstools/css-tokenizer"),r=require("@csstools/css-parser-algorithms"),t=require("@csstools/css-color-parser");const c=/\bcontrast-color\(/i,n=/^contrast-color$/i,basePlugin=s=>({postcssPlugin:"postcss-contrast-color-function",prepare:()=>({postcssPlugin:"postcss-contrast-color-function",Declaration(i){const a=i.value;if(!c.test(a))return;if(e.hasFallback(i))return;if(e.hasSupportsAtRuleAncestor(i,c))return;const l=o.tokenize({css:a}),u=r.replaceComponentValues(r.parseCommaSeparatedListOfComponentValues(l),(s=>{if(!r.isFunctionNode(s)||!n.test(s.getName()))return;const e=t.color(s);return e&&!e.syntaxFlags.has(t.SyntaxFlag.HasNoneKeywords)?t.serializeRGB(e):void 0})),p=r.stringify(u);p!==a&&(i.cloneBefore({value:p}),s?.preserve||i.remove())}})});basePlugin.postcss=!0;const postcssPlugin=e=>{const o=Object.assign({enableProgressiveCustomProperties:!0,preserve:!0},e);return o.enableProgressiveCustomProperties&&o.preserve?{postcssPlugin:"postcss-contrast-color-function",plugins:[s(),basePlugin(o)]}:basePlugin(o)};postcssPlugin.postcss=!0,module.exports=postcssPlugin;
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
import r from"@csstools/postcss-progressive-custom-properties";import{hasFallback as o,hasSupportsAtRuleAncestor as e}from"@csstools/utilities";import{isTokenIdent as s,tokenize as t,TokenType as n,NumberType as c}from"@csstools/css-tokenizer";import{isFunctionNode as a,isWhitespaceNode as l,isCommentNode as u,isTokenNode as i,replaceComponentValues as p,parseCommaSeparatedListOfComponentValues as f,FunctionNode as m,TokenNode as v,stringify as C}from"@csstools/css-parser-algorithms";import{color as E,serializeRGB as d,serializeP3 as g}from"@csstools/css-color-parser";import{contrast_ratio_wcag_2_1 as P}from"@csstools/color-helpers";const h=/^contrast-color$/i;function parseContrastColor(r){if(!a(r)||!h.test(r.getName()))return!1;const o=r.value.filter((r=>!l(r)&&!u(r)));if(o.length>2)return!1;const e=o[0],t=o[1];return!!e&&(t?!(!i(t)||!s(t.value)||"max"!==t.value[4].value.toLowerCase())&&[e,"max"]:[e])}var R;!function(r){r[r.MORE=0]="MORE",r[r.LESS=1]="LESS",r[r.NO_PREFERENCE=2]="NO_PREFERENCE"}(R||(R={}));const N=/\bcontrast-color\(/i;function transformContrastColor(r,o,e=0){const s=p(f(t({css:r})),(r=>{const e=parseContrastColor(r);if(!e)return;const[s,t]=e;if("max"===t){const o=E(r);if(!o)return;return d(o,!0)}if(t)return;const a=E(new m([n.Function,"contrast-color(",-1,-1,{value:"contrast-color"}],[n.CloseParen,")",-1,-1,void 0],[s,new v([n.Ident,"max",-1,-1,{value:"max"}])]));if(!a)return;if(o===R.MORE)return d(a,!0);const l=E(s);if(!l)return;let u=0;const i=E(d(l,!0));if(!i)return;{const r=P(i.channels,[0,0,0]),e=P(i.channels,[1,1,1]);u=o===R.LESS?r>=e?.3:.9:r>=e?.2:.95}const p=E(new m([n.Function,"oklch(",-1,-1,{value:"oklch"}],[n.CloseParen,")",-1,-1,void 0],[new v([n.Ident,"from",-1,-1,{value:"from"}]),s,new v([n.Number,u.toString(),-1,-1,{value:u,type:c.Number}]),new v([n.Ident,"c",-1,-1,{value:"c"}]),new v([n.Ident,"h",-1,-1,{value:"h"}])]));if(!p)return;const f=E(d(p,!0));if(!f)return;return P(i.channels,f.channels)<4.5?d(a,!0):g(p,!0)})),a=C(s);return a===r?r:e>10?a:N.test(a)?transformContrastColor(a,o,e+1):a}const basePlugin=r=>({postcssPlugin:"postcss-contrast-color-function",prepare:()=>({postcssPlugin:"postcss-contrast-color-function",Declaration(s,{atRule:t}){const n=s.parent;if(!n)return;if(!N.test(s.value))return;if(o(s))return;if(e(s,N))return;const c=transformContrastColor(s.value,R.NO_PREFERENCE);if(c===s.value)return;const a=transformContrastColor(s.value,R.LESS);if(a===s.value)return;const l=transformContrastColor(s.value,R.MORE);if(l!==s.value){if(s.cloneBefore({value:c}),c!==a){const o=n.clone();o.removeAll(),o.append(s.clone({value:a}));const e=t({name:"media",params:"(prefers-contrast: less)",source:n.source});if(e.append(o),r?.preserve){const r=t({name:"supports",params:"not (color: contrast-color(red max))",source:n.source});r.append(e),n.after(r)}else n.after(e)}if(c!==l){const o=n.clone();o.removeAll(),o.append(s.clone({value:l}));const e=t({name:"media",params:"(prefers-contrast: more)",source:n.source});if(e.append(o),r?.preserve){const r=t({name:"supports",params:"not (color: contrast-color(red max))",source:n.source});r.append(e),n.after(r)}else n.after(e)}r?.preserve||s.remove()}}})});basePlugin.postcss=!0;const postcssPlugin=o=>{const e=Object.assign({enableProgressiveCustomProperties:!0,preserve:!0},o);return e.enableProgressiveCustomProperties&&e.preserve?{postcssPlugin:"postcss-contrast-color-function",plugins:[r(),basePlugin(e)]}:basePlugin(e)};postcssPlugin.postcss=!0;export{postcssPlugin as default};
1+
import s from"@csstools/postcss-progressive-custom-properties";import{hasFallback as o,hasSupportsAtRuleAncestor as r}from"@csstools/utilities";import{tokenize as t}from"@csstools/css-tokenizer";import{replaceComponentValues as e,parseCommaSeparatedListOfComponentValues as c,isFunctionNode as n,stringify as i}from"@csstools/css-parser-algorithms";import{color as p,SyntaxFlag as l,serializeRGB as a}from"@csstools/css-color-parser";const u=/\bcontrast-color\(/i,m=/^contrast-color$/i,basePlugin=s=>({postcssPlugin:"postcss-contrast-color-function",prepare:()=>({postcssPlugin:"postcss-contrast-color-function",Declaration(f){const g=f.value;if(!u.test(g))return;if(o(f))return;if(r(f,u))return;const v=t({css:g}),P=e(c(v),(s=>{if(!n(s)||!m.test(s.getName()))return;const o=p(s);return o&&!o.syntaxFlags.has(l.HasNoneKeywords)?a(o):void 0})),b=i(P);b!==g&&(f.cloneBefore({value:b}),s?.preserve||f.remove())}})});basePlugin.postcss=!0;const postcssPlugin=o=>{const r=Object.assign({enableProgressiveCustomProperties:!0,preserve:!0},o);return r.enableProgressiveCustomProperties&&r.preserve?{postcssPlugin:"postcss-contrast-color-function",plugins:[s(),basePlugin(r)]}:basePlugin(r)};postcssPlugin.postcss=!0;export{postcssPlugin as default};

plugins/postcss-contrast-color-function/package.json

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,6 @@
4848
"dist"
4949
],
5050
"dependencies": {
51-
"@csstools/color-helpers": "^5.0.2",
5251
"@csstools/css-color-parser": "^3.0.8",
5352
"@csstools/css-parser-algorithms": "^3.0.4",
5453
"@csstools/css-tokenizer": "^3.0.3",

0 commit comments

Comments
 (0)