如何建立动态和可配置的配色方案?
在这篇文章中,我想分享关于如何在 CSS 中管理多种配色方案的想法。
我们将构建具有自定义属性和 的可访问颜色系统 calc()
,以制作适应用户偏好的网页,同时保持最小的创作体验。我们从基本品牌颜色开始,并从中构建一个变体系统:2 种文本颜色、4 种表面颜色和匹配的阴影。
通常,brand color 已经确定并以hex
或rgb
形式提供。此 GUI 挑战的基本brand color为 #0af
. 首先,对于这个颜色系统,十六进制值需要转换为hsl
。
* {
--brand: #0af;
--brand: hsl(200 100% 50%);
}
为了实现brand color 变暗或变亮的概念,比如 20%,hsl 颜色值的 3 个通道需要提取到它们自己的自定义属性中,如下所示:
* {
--brand-hue: 200;
--brand-saturation: 100%;
--brand-lightness: 50%;
}
CSS 可以对这些颜色属性进行数学运算,例如calc(var(--brand-lightness) - 20%)
将亮度值降低 20%。这是构建配色方案的基础,因为 CSS 可以通过调整 hsl
饱和度和亮度量将所有颜色保持在同一色调系列中。
每个颜色变体都将标有其匹配方案,在这种情况下,每个变体都附加-light
.
从brand颜色开始,通过将--brand-hue,--brand-saturation和--brand-lightness
自定义属性包装在 hsl()
函数括号内进行重建,无需任何计算:
* {
--brand-light: hsl(var(--brand-hue) var(--brand-saturation) var(--brand-lightness));
}
接下来,配色方案的基本要素需要文本颜色。在浅色主题中,文本应该非常暗。请注意以下颜色的亮度如何很低,远低于 50%。
* {
--text1-light: hsl(var(--brand-hue) var(--brand-saturation) 10%);
--text2-light: hsl(var(--brand-hue) 30% 30%);
}
--text1-light
,因为它在 10% 的亮度下非常暗,所以保持 100% 的饱和度,所以品牌颜色仍然可以窥视到深海军蓝中。
Tips: 大多数用户无法告诉您它不是黑色,但他们会注意到它以某种方式感觉和谐。秘诀是将黑色和白色专门用于最亮的高光和最暗的阴影。
surface颜色是文本所在的背景、边框和其他装饰表面。在浅色主题中,这些是浅色,而不是深色的文本颜色。要使用 hsl 创建浅色,我们将在第三个亮度值中使用更高的百分比值。我们还将降低饱和度,使浅灰色看起来不会太着色。
* {
--surface1-light: hsl(var(--brand-hue) 25% 90%);
--surface2-light: hsl(var(--brand-hue) 20% 99%);
--surface3-light: hsl(var(--brand-hue) 20% 92%);
--surface4-light: hsl(var(--brand-hue) 20% 85%);
}
创建了 4 种表面颜色,因为装饰颜色往往需要更多变体,用于交互时刻,如:focus
或:hover
或创建纸层的外观。在这些情况下,最好将--surface2-light
悬停转换为--surface3-light
,因此悬停会增加对比度(99% 亮度到 92% 亮度;使其更暗)。
配色方案中的阴影是超越的,但为效果添加了栩栩如生的自然效果,并帮助它从不切实际的黑色阴影中脱颖而出。为此,阴影的颜色将使用色调自定义属性,色调略微饱和但仍然很暗。基本上建立一个非常暗的略带蓝色的阴影。
* {
--surface-shadow-light: var(--brand-hue) 10% 20%;
--shadow-strength-light: .02;
}
--surface-shadow-light
没有包含在 hsl 函数中。这是因为该--shadow-strength
值将被组合以创建一些不透明度,并且 CSS 需要这些部分才能执行计算。
综上可见,浅色主题的制作方法如下:
* {
--brand-light: hsl(var(--brand-hue) var(--brand-saturation) var(--brand-lightness));
--text1-light: hsl(var(--brand-hue) var(--brand-saturation) 10%);
--text2-light: hsl(var(--brand-hue) 30% 30%);
--surface1-light: hsl(var(--brand-hue) 25% 90%);
--surface2-light: hsl(var(--brand-hue) 20% 99%);
--surface3-light: hsl(var(--brand-hue) 20% 92%);
--surface4-light: hsl(var(--brand-hue) 20% 85%);
--surface-shadow-light: var(--brand-hue) 10% calc(var(--brand-lightness) / 5);
--shadow-strength-light: .02;
}
下面给出一个实例:
<figure class="brand">
<div class="swatch"></div>
<figcaption>Brand</figcaption>
</figure>
<figure class="text1">
<div class="swatch"></div>
<figcaption>Text 1</figcaption>
</figure>
<figure class="text2">
<div class="swatch"></div>
<figcaption>Text 2</figcaption>
</figure>
<figure class="surface1">
<div class="swatch"></div>
<figcaption>Surface 1</figcaption>
</figure>
<figure class="surface2">
<div class="swatch"></div>
<figcaption>Surface 2</figcaption>
</figure>
<figure class="surface3">
<div class="swatch"></div>
<figcaption>Surface 3</figcaption>
</figure>
<figure class="surface4">
<div class="swatch"></div>
<figcaption>Surface 4</figcaption>
</figure>
<figure class="shadow">
<div class="swatch"></div>
<figcaption>Shadow</figcaption>
</figure>
* {
--hue: 200;
--saturation: 100%;
--lightness: 50%;
--brand-dim: hsl(var(--hue) calc(var(--saturation) / 1.25) calc(var(--lightness) / 1.25));
--text1-dim: hsl(var(--hue) 15% 75%);
--text2-dim: hsl(var(--hue) 10% 61%);
--surface1-dim: hsl(var(--hue) 10% 20%);
--surface2-dim: hsl(var(--hue) 10% 25%);
--surface3-dim: hsl(var(--hue) 5% 30%);
--surface4-dim: hsl(var(--hue) 5% 35%);
--surface-shadow-dim: var(--hue) 30% 13%;
--shadow-strength-dim: .2;
}
.brand .swatch { background: var(--brand-dim) }
.text1 .swatch { background: var(--text1-dim) }
.text2 .swatch { background: var(--text2-dim) }
.surface1 .swatch { background: var(--surface1-dim) }
.surface2 .swatch { background: var(--surface2-dim) }
.surface3 .swatch { background: var(--surface3-dim) }
.surface4 .swatch { background: var(--surface4-dim) }
.shadow .swatch { background: hsl(var(--surface-shadow-dim) / var(--shadow-strength-dim)) }
figure {
display: flex;
gap: 1ch;
place-items: center;
font-size: 2rem;
font-weight: 200;
}
.swatch {
flex-shrink: 0;
inline-size: 3ch;
aspect-ratio: 1;
border-radius: 50%;
box-shadow: inset 0 0 0 1px hsl(0 0% 50% / 25%);
}
@media (max-width: 1500px) {
figcaption {
display: none;
}
}
* {
box-sizing: border-box;
margin: 0;
}
html {
block-size: 100%;
}
body {
min-block-size: 100%;
font-family: system-ui, sans-serif;
display: flex;
place-content: center;
gap: 3ch;
padding: 5ch;
}
大多数品牌都没有以深色主题开始,它是其主要主题的变体,通常是较浅的主题。另一方面,用户通常为不同的环境选择一个黑暗的主题,比如夜间。这些因素使我在黑暗主题中牢记两件事:
浅色主题使用 3 个brand的 hsl 颜色通道值而没有更改,深色主题则没有。饱和度减半,亮度相对降低 50%。
* {
--brand-dark: hsl(
var(--brand-hue)
calc(var(--brand-saturation) / 2)
calc(var(--brand-lightness) / 1.5)
);
}
在深色主题中,文本颜色应为浅色。以下颜色具有较高的亮度值,使它们更接近白色。
* {
--text1-dark: hsl(var(--brand-hue) 15% 85%);
--text2-dark: hsl(var(--brand-hue) 5% 65%);
}
在深色主题中,表面颜色应为深色。以下颜色的亮度和饱和度较低,第一个表面最暗,为 10%。
* {
--surface1-dark: hsl(var(--brand-hue) 10% 10%);
--surface2-dark: hsl(var(--brand-hue) 10% 15%);
--surface3-dark: hsl(var(--brand-hue) 5% 20%);
--surface4-dark: hsl(var(--brand-hue) 5% 25%);
}
在黑暗主题中,很难看到阴影。这是有道理的,因为很难使已经相当黑暗的东西变暗。这是--shadow-strength-dark
非常方便的地方,因为它允许我们通过改变一个变量来使阴影变暗。
* {
--surface-shadow-dark: var(--brand-hue) 50% 3%;
--shadow-strength-dark: .8;
}
另外,看看那个阴影中有多少饱和度。当您查看界面时,您能注意到颜色吗?尝试从 devtools 中删除饱和度,您更喜欢哪个?!
* {
--brand-dark: hsl(var(--brand-hue) calc(var(--brand-saturation) / 2) calc(var(--brand-lightness) / 1.5));
--text1-dark: hsl(var(--brand-hue) 15% 85%);
--text2-dark: hsl(var(--brand-hue) 5% 65%);
--surface1-dark: hsl(var(--brand-hue) 10% 10%);
--surface2-dark: hsl(var(--brand-hue) 10% 15%);
--surface3-dark: hsl(var(--brand-hue) 5% 20%);
--surface4-dark: hsl(var(--brand-hue) 5% 25%);
--surface-shadow-dark: var(--brand-hue) 50% 3%;
--shadow-strength-dark: .8;
}
这种配色方案是关于协调亮度和饱和度的。应该有足够的饱和度来保持色调可见,但也应该勉强通过对比度分数,因为无论如何它都是暗淡和低对比度的。
* {
--brand-dim: hsl(
var(--brand-hue)
calc(var(--brand-saturation) / 1.25)
calc(var(--brand-lightness) / 1.25)
);
}
* {
--text1-dim: hsl(var(--brand-hue) 15% 75%);
--text2-dim: hsl(var(--brand-hue) 10% 61%);
}
* {
--surface1-dim: hsl(var(--brand-hue) 10% 20%);
--surface2-dim: hsl(var(--brand-hue) 10% 25%);
--surface3-dim: hsl(var(--brand-hue) 5% 30%);
--surface4-dim: hsl(var(--brand-hue) 5% 35%);
}
* {
--surface-shadow-dim: var(--brand-hue) 30% 13%;
--shadow-strength-dim: .2;
}
* {
--brand-dim: hsl(var(--brand-hue) calc(var(--brand-saturation) / 1.25) calc(var(--brand-lightness) / 1.25));
--text1-dim: hsl(var(--brand-hue) 15% 75%);
--text2-dim: hsl(var(--brand-hue) 10% 61%);
--surface1-dim: hsl(var(--brand-hue) 10% 20%);
--surface2-dim: hsl(var(--brand-hue) 10% 25%);
--surface3-dim: hsl(var(--brand-hue) 5% 30%);
--surface4-dim: hsl(var(--brand-hue) 5% 35%);
--surface-shadow-dim: var(--brand-hue) 30% 13%;
--shadow-strength-dim: .2;
}
随着颜色的预定义完成,是时候将它们变成与方案无关的属性了。我的意思是,作为这个配色方案项目中的 CSS 作者,应该很少需要访问特定配色方案的值。我想让它更容易留在主题中。
为了实现这一点,颜色方案的使用应该完全通过通用自定义属性完成,我们将在稍后定义。这样,使用设计变量的人永远不必担心当前设置的是哪种配色方案,他们只需要使用表面和文本颜色。而不是color: var(--text1-light)使用color: var(--text1). 所有颜色的调整和旋转都在 CSS 中完成了更高的级别。
深入了解以下代码块中灯光主题的连接样式,将通用自定义属性与灯光主题特定颜色连接起来。现在所有用途都var(--brand)将使用浅色品牌颜色。
:root {
color-scheme: light;
--brand: var(--brand-light);
--text1: var(--text1-light);
--text2: var(--text2-light);
--surface1: var(--surface1-light);
--surface2: var(--surface2-light);
--surface3: var(--surface3-light);
--surface4: var(--surface4-light);
--surface-shadow: var(--surface-shadow-light);
--shadow-strength: var(--shadow-strength-light);
}
@media (prefers-color-scheme: dark) {
:root {
color-scheme: dark;
--brand: var(--brand-dark);
--text1: var(--text1-dark);
--text2: var(--text2-dark);
--surface1: var(--surface1-dark);
--surface2: var(--surface2-dark);
--surface3: var(--surface3-dark);
--surface4: var(--surface4-dark);
--surface-shadow: var(--surface-shadow-dark);
--shadow-strength: var(--shadow-strength-dark);
}
}
[color-scheme="light"] {
color-scheme: light;
--brand: var(--brand-light);
--text1: var(--text1-light);
--text2: var(--text2-light);
--surface1: var(--surface1-light);
--surface2: var(--surface2-light);
--surface3: var(--surface3-light);
--surface4: var(--surface4-light);
--surface-shadow: var(--surface-shadow-light);
--shadow-strength: var(--shadow-strength-light);
}
[color-scheme="dark"] {
color-scheme: dark;
--brand: var(--brand-dark);
--text1: var(--text1-dark);
--text2: var(--text2-dark);
--surface1: var(--surface1-dark);
--surface2: var(--surface2-dark);
--surface3: var(--surface3-dark);
--surface4: var(--surface4-dark);
--surface-shadow: var(--surface-shadow-dark);
--shadow-strength: var(--shadow-strength-dark);
}
[color-scheme="dim"] {
color-scheme: dark;
--brand: var(--brand-dim);
--text1: var(--text1-dim);
--text2: var(--text2-dim);
--surface1: var(--surface1-dim);
--surface2: var(--surface2-dim);
--surface3: var(--surface3-dim);
--surface4: var(--surface4-dim);
--surface-shadow: var(--surface-shadow-dim);
--shadow-strength: var(--shadow-strength-dim);
}