WebGL Tutorial
and more

CSS Colors

撰写时间:2025-03-01

修订时间:2025-04-10

Color Names

下面是CSS Color Module Level 4所定义的命名颜色,同时换算为多种最常用的格式。

table > thead > tr > th { position: sticky; top: 0; z-index: 1; line-height: 3em; } table > tbody > tr > td:nth-child(2) { display: block; box-sizing: border-box; margin: 1em; width: 50px; aspect-ratio : 1.618 / 1; border-width: 0; border-radius: 0.3em; } table > tbody > tr > td:nth-child(1) { text-align: center; }
import { GLColors, COLOR_NAMES } from '/js/esm/GLColors.js'; import { DOMUtils } from '/js/esm/DOMUtils.js'; import { ColorUtils } from '/js/esm/ColorUtils.js'; function genTable() { let ths = ['Index', 'Color Block', 'Color Name', 'Hex', 'Decimal', 'Clamped RGB', 'HSL']; let cells = []; let index = 1; for (let colorName in COLOR_NAMES) { let rowData = []; rowData.push(index++); rowData.push(''); rowData.push(colorName.toLowerCase()); rowData.push(COLOR_NAMES[colorName]); let decimal = ColorUtils.HexToDecimal(COLOR_NAMES[colorName]); rowData.push(decimal.join(', ')); let clampedRGB = GLColors.FromFullHex(COLOR_NAMES[colorName]); clampedRGB.splice(-1, 1); let twoDigitsArr = clampedRGB.map(ele => ele.toFixed(2)); rowData.push(twoDigitsArr.join(', ')); let hslArr = ColorUtils.ClampedRgbToHsl(...clampedRGB); let [h, s, l] = hslArr; h = parseInt(h); s = parseInt(s) + '%'; l = parseInt(l) + '%'; rowData.push(h + ', ' + s + ', ' + l); cells.push(rowData); } let str = DOMUtils.GenTableStr(ths, cells); document.body.insertAdjacentHTML('afterbegin', str); } function styleTable() { let colorValues = Object.values(COLOR_NAMES); let table = document.querySelector('table'); let firstTds = table.querySelectorAll('tbody tr td:nth-child(2)'); console.log(firstTds); firstTds.forEach((td, idx) => { td.style.backgroundColor = colorValues[idx]; }); } genTable(); styleTable();

HSL

HSL颜色方案 (color scheme) 使用色相 (hue), 饱和度 (saturation), 及亮度 (lightness) 此3种要素来表示一个颜色,与我们日常描绘一种颜色时所使用的颜色、鲜艳度、亮度的习惯比较吻合。

hsl函数的参数如下:

hsl(hue, saturation, lightness);

对于hsl函数中各参数的数据类型,可使用以下2种表达方式:

hsl(120deg, 80%, 20%); hsl(230, 30%, 90%);

后面两个成分应使用百分比的格式,否则浏览器可能无法正确解析。

下面代码演示了hslrgb两种函数各参数的实时转换。

import { ColorUtils } from "/js/esm/ColorUtils.js"; let hslModal = { hue: 215, saturation: 50, lightness: 50 }; let hslHueEle = document.getElementById('hsl-hue'); let hslSaturationEle = document.getElementById('hsl-saturation'); let hslLightnessEle = document.getElementById('hsl-lightness'); let clampedRgbREle = document.getElementById('clamped-rgb-r'); let clampedRgbGEle = document.getElementById('clamped-rgb-g'); let clampedRgbBEle = document.getElementById('clamped-rgb-b'); let rgbREle = document.getElementById('rgb-r'); let rgbGEle = document.getElementById('rgb-g'); let rgbBEle = document.getElementById('rgb-b'); updateView(); function updateView(except) { hslHueEle.value = hslModal.hue; hslSaturationEle.value = hslModal.saturation; hslLightnessEle.value = hslModal.lightness; let clampedRgbArr = ColorUtils.HslToClampedRgb(hslModal.hue, hslModal.saturation, hslModal.lightness); if (except !== 'clampedR') { clampedRgbREle.value = clampedRgbArr[0].toFixed(2); } if (except !== 'clampedG') { clampedRgbGEle.value = clampedRgbArr[1].toFixed(2); } if (except !== 'clampedB') { clampedRgbBEle.value = clampedRgbArr[2].toFixed(2); } let rgbArr = ColorUtils.HslToRgb(hslModal.hue, hslModal.saturation, hslModal.lightness); if (except !== 'r') { rgbREle.value = rgbArr[0]; } if (except !== 'g') { rgbGEle.value = rgbArr[1]; } if (except !== 'b') { rgbBEle.value = rgbArr[2]; } let colorBlock = document.getElementById('color-block'); colorBlock.style.backgroundColor = `hsl(${hslModal.hue}deg, ${hslModal.saturation}%, ${hslModal.lightness}%)`; } hslHueEle.oninput = (evt) => { hslModal.hue = parseInt(evt.target.value); updateView(); }; hslSaturationEle.oninput = (evt) => { hslModal.saturation = parseInt(evt.target.value); updateView(); }; hslLightnessEle.oninput = (evt) => { hslModal.lightness = parseInt(evt.target.value); updateView(); }; clampedRgbREle.oninput = (evt) => { updateModalFromRGBView('clamped'); updateView('clampedR'); }; clampedRgbGEle.oninput = (evt) => { updateModalFromRGBView('clamped'); updateView('clampedG'); }; clampedRgbBEle.oninput = (evt) => { updateModalFromRGBView('clamped'); updateView('clampedB'); }; rgbREle.oninput = (evt) => { updateModalFromRGBView(); updateView('r'); }; rgbGEle.oninput = (evt) => { updateModalFromRGBView(); updateView('g'); }; rgbBEle.oninput = (evt) => { updateModalFromRGBView(); updateView('b'); }; function updateModalFromRGBView(rgbType) { let r, g, b; let h, s, l; if (rgbType === 'clamped') { r = parseFloat(clampedRgbREle.value); g = parseFloat(clampedRgbGEle.value); b = parseFloat(clampedRgbBEle.value); [h, s, l] = ColorUtils.ClampedRgbToHsl(r, g, b); } else { r = parseFloat(rgbREle.value); g = parseFloat(rgbGEle.value); b = parseFloat(rgbBEle.value); [h, s, l] = ColorUtils.RgbToHsl(r, g, b); } hslModal.hue = Math.round(h); hslModal.saturation = Math.round(s); hslModal.lightness = Math.round(l); }
: (deg, %, %)
: (, , )
: (, , )
form { margin: 0em; } form div { margin: 0.5em 0; } form label { color: lightgreen; display: inline-block; min-width: 80px; text-align: right; } form input[type=number] { border: 1px solid gray; color: #ccc; background-color: #2b2b2b; margin-right: 0.1em; } form > div > span > input { margin: 0 0 0 1em; } form > div > span > span { color: gray; } form > div > span > span:first-of-type { margin-left: 0.5em; } form > div > span > span:last-of-type { margin-left: 0.5em; } form #color-block { width: 235px; height: 140px; border: 1px solid gray; margin-left: 100px; margin-top: 1em; }

对于hsl函数,如果lightness的值为100%,则产生纯白色;如果lightness的值为0%,则产生黑色。此时huesaturation将不起作用。

RGB颜色方案强调颜色通道,因此若要单独增减某个颜色通道的数值,使用该颜色方案则比较便利。

相对颜色

相对颜色功能可让我们将任意颜色转换为另外一种颜色空间的数值,然后单独操控各种颜色通道。

检测浏览器是否支持?

@supports (color: rgb(from red r g b)) { p { background: green; } } @supports not (color: rgb(from red r g b)) { p { background: red; } } p { margin: 0; padding: 1em; }

Is relative colors supported?

改变各个通道值:

p { --my-color: #E1C076; color: hsl(from var(--my-color) calc(h + 20) calc(s * 0.5) calc(l * 0.8)); }

CSS relative colors.

色相值增加20,饱和度只取50%,亮度只取80%

在使用calc函数时,使用小数点的形式来与通道值相乘;若使用百分比,则表示取自通道值域的百分比,与通道值没有关系。

p { --my-color: green; color: hsl(from var(--my-color) h s calc(50%)); }

CSS relative colors.

from可跟有任何有效的颜色表达式。下面使用rgb()函数的方式:

p { color: hsl(from rgb(128, 255, 128) h 50% 80%); }

CSS relative colors.

from后面如果需要使用十六进制表示颜色时,表达式不能带有双引号。

p { color: hsl(from #9933CC h 50% 80%); }

CSS relative colors.

通道值可直接用数值替换,或照抄表示通道的字母以保留:

p { --my-color: hsl(150, 70%, 80%); color: hsl(from var(--my-color) 220 s l); }

CSS relative colors.

将色相值使用数值220直接替换,饱和度及亮度则予以保留。

改变不透明度:

p { --my-color: #E1C076; color: hsl(from var(--my-color) h s l / 90%); }

CSS relative colors.

currentcolor

可用currentcolor来取出当前标签color属性值。

p { margin: 0; padding: 1em; color: hsl(150, 75%, 80%); background-color: hsl(from currentcolor calc(h + 120) s l / 20%); }

CSS relative colors.

颜色融合

在特定的颜色空间,将两个颜色进行融合,得到一个新的颜色值。可指定参与融合颜色的成分占比。

:root { --block-side: 60px; --bg-color1: magenta; --color1-amt: 50%; --bg-color2: cyan; --color2-amt: 50%; } #grid { display: grid; grid-template-areas: "src1 src2" "dst dst"; grid-template-columns: var(--block-side) var(--block-side); grid-template-rows: var(--block-side) var(--block-side); gap: 0.5em; min-height: 150px; justify-content: center; align-content: center; } #src1 {grid-area: src1} #src2 {grid-area: src2} #dst {grid-area: dst; justify-self: center; width: var(--block-side);} div > div { border-radius: 10%; } #src1 {background-color: var(--bg-color1);} #src2 {background-color: var(--bg-color2);} #dst {background-color: color-mix(in srgb, var(--bg-color1) var(--color1-amt), var(--bg-color2) var(--color2-amt));}

对比度比率

WCAG AA级对文本(包括图像中的文本),要求其对比度比率contrast ratio)至少为4.5:1,而较大字号的文本,要求其对比度比率至少为3:1

WCAG AAA级对文本(包括图像中的文本),要求其对比度比率至少为7:1,而较大字号的文本,要求其对比度比率至少为4.5:1

计算对比度比率的公式为:

(L1 + 0.05) / (L2 + 0.05)

其中L1为较亮颜色的相对亮度(relative luminance),L2为较暗颜色的相对亮度。以sRGB颜色方案为例,相对亮度L的公式为:

L = 0.2126 * R + 0.7152 * G + 0.0722 * B

R, G, B定义如下:

if RsRGB <= 0.04045 then R = RsRGB/12.92 else R = ((RsRGB+0.055)/1.055) ^ 2.4 if GsRGB <= 0.04045 then G = GsRGB/12.92 else G = ((GsRGB+0.055)/1.055) ^ 2.4 if BsRGB <= 0.04045 then B = BsRGB/12.92 else B = ((BsRGB+0.055)/1.055) ^ 2.4

常量值0.04045在之前版本中曾为0.03928,后面已改为现值。

RsRGB, GsRGB, BsRGB分别定义如下:

RsRGB = R8bit/255 GsRGB = G8bit/255 BsRGB = B8bit/255

取本页面暗黑主题的文本前景色rgb(172, 183, 196)及背景色rgb(43, 43, 43)为例,则对比度比率为:

function getRelativeLuminance(r, g, b) { let R = calcChanel(r); let G = calcChanel(g); let B = calcChanel(b); let L = 0.2126 * R + 0.7152 * G + 0.0722 * B; return L; function calcChanel(chanelBit) { let chanel_sRGB = chanelBit / 255; if (chanel_sRGB <= 0.04045 ) { return chanel_sRGB / 12.92; } else { return Math.pow((chanel_sRGB + 0.055 / 1.055), 2.4); } } } function getContrastRatio(L1, L2) { return (Math.max(L1, L2) + 0.05) / (Math.min(L1, L2) + 0.05); } let L1 = getRelativeLuminance(172, 183, 196); let L2 = getRelativeLuminance(43, 43, 43); let cr = getContrastRatio(L1, L2); pc.log(cr);

因此,在暗黑主题下,其对比度比率已达到WCAG AAA级。

颜色空间的转换

转换为sRGB

在不使用相对颜色的情况下,不管使用何种颜色空间来指定颜色,浏览器经过计算后,在内部都会自动转换为以sRGB的形式来存储其值。利用此特性,调用getComputedStyle函数可直接得到sRGB的结果。

#color-block { background-color: #889966CC; } #container { display: grid; grid-template-columns: 100px auto; grid-template-rows: 100px; column-gap: 1em; height: 100vh; place-content: center; } #output { align-self: center; display: grid; grid-template-columns: 30px 150px; gap: 1em; } #output > * { padding: 0.5em; } label { color: hwb(from #336699 h 20% 0); } input[type=text] { color: #eee; background-color: #333; border: 1px solid #555; }
let colorBlockEle = document.getElementById("color-block"); let labelEle = document.getElementsByTagName('label')[0]; let bgColorStr = getComputedStyle(colorBlockEle).getPropertyValue('background-color'); bgColorStr = bgColorStr.replace(/(rgba?\()(.+)(\))/, '$2'); if (bgColorStr.match(/(\d{1,3}(\,\s)?){3}0\.\d/g)) { labelEle.textContent = 'rgba:'; } else { labelEle.textContent = 'rgb:'; } let rgbOutput = document.getElementById("rgb-output"); rgbOutput.value = bgColorStr;

oklab

oklab颜色空间能模拟出漫反射的光照效果。虽然其a, b轴以色温的方式来指定色相值不太直观,但其l轴所代表的亮度却比较接近人们对光线的感知。

下面使用滑块来控制其亮度值。

#color-block { background-color: green; aspect-ratio: 1 / 1; margin: 1em; box-sizing: border-box; } #container { display: grid; grid-template-columns: minmax(100px, 100vh) minmax(220px, 0.8fr); height: 100vh; place-content: center; } #panel { align-self: center; display: grid; grid-template-columns: auto } input[type=range] { -webkit-appearance: none; background-color: transparent; } input[type=range]::-webkit-slider-container { /* border: 1px solid red; */ } input[type=range]::-webkit-slider-runnable-track { background: hsl(51, 25%, 50%); box-shadow: 5px 5px 5px #333, inset -5px -5px 3px #ccc; border-radius: 0.5em; height: 10px; align-self: center; display: flex; } input[type=range]::-webkit-slider-thumb { -webkit-appearance: none; width: 20px; height: 20px; border-radius: 50%; background: steelblue; align-self: center; }
let colorBlock = document.getElementById('color-block'); let bgColorStr = getComputedStyle(colorBlock).getPropertyValue('background-color'); let slider = document.getElementById('oklab-slider'); slider.oninput = evt => { colorBlock.style.backgroundColor = `oklab(from ${bgColorStr} ${slider.value}% a b)`; };

先使用具名颜色的方式来指定颜色,再利用相对颜色转换为oklab颜色空间,最后让滑块单独控制其亮度通道值。

您是否仍在使用传统的sRGB色彩空间?

if (window.matchMedia("(color-gamut: p3)").matches) { document.querySelector('div').textContent = 'You have a wide-range gamut display that supports DCI P3 color space.'; }

运行代码后,如果有文字出现,则意味着您有机会、但可能从未体验更加灵艳的色彩空间。因为DCI P3伽马 (gamut)值范围(即,能在设备上展现出能被人眼所感知的色彩范围),比传统的sRGB要更加丰富。

div { background-color: rgb(125, 125, 125); width: 100px; aspect-ratio: 1 / 1; } @supports (color: color(display-p3 0.0 0.0 0.0)) { div { background-color: color(display-p3 -0.6112 1.0079 -0.2192); } }

当看到亮绿色,证明您的设备支持display-p3;如果设备不支持,则会只看到一个sRGB颜色空间的灰色。下面是两者能展现的色彩范围的对比图。

P3-prim-sec
P3-prim-sec

上图中,实心的色球表示display-p3的色彩范围,而半透明的色球表示sRGB的色彩范围。前者比后者的范围要广。

Safari支持Canvas Color Spaces,下面的代码检测其是否支持display-p3的颜色空间。

function isSupportP3Canvas() { let canvas = document.createElement("canvas"); try { let context = canvas.getContext("2d", { colorSpace: "display-p3" }); return context.getContextAttributes().colorSpace == "display-p3"; } catch { console.log('error!'); } return false; } if (isSupportP3Canvas()) { document.querySelector('p').textContent = 'Canvas supports P3 colors!'; }

canvas上使用display-p3来绘制图像。

let canvas = document.getElementById("canvas"); let context = canvas.getContext("2d", { colorSpace: "display-p3" }); let position = 0; for (let green of [1, 0]) { for (let blue of [1, 0]) { for (let red of [1, 0]) { context.fillStyle = `color(display-p3 ${red} ${green} ${blue})`; context.fillRect(position, position, 40, 40); position += 20; } } }

下面的代码,轮流使用两个颜色空间中的绿色来绘制图像。

let canvas = document.getElementById("canvas"); let context = canvas.getContext("2d", { colorSpace: "display-p3" }); const COLORS = ["#0f0", "color(display-p3 0 1 0)"]; for (let y = 20; y < 180; y += 20) { let color = COLORS[(y / 20) % 2]; console.log(color); context.fillStyle = color; context.fillRect(20, y, 160, 20); }

在终端面板中可看到,奇行是display-p3的绿色,偶行是sRGB的绿色。从输出面板中可看出,display-p3能表现的绿色比sRGB能表现的绿色要更为鲜艳。

在Chrome中得到了同样的效果。

因此,拥抱display-p3,它能让我们的世界更加美不胜收。

参考资源

  1. CSS Color Module Level 4
  2. CSS Color Module Level 5
  3. Converting HSL Colors to sRGB
  4. WebAIM Contrast Checker
  5. WCAG 2.1
  6. Contrast and Color Accessibility
  7. Contrast and Color Accessibility
  8. Improving Color on the Web
  9. Wide Gamut 2D Graphics using HTML Canvas