unocss中提供的调色板有很多种颜色跟深度,如c-red-100、c-blue-800等等,但基本都是固定的50~950的11种深度颜色,如果需要添加新的深度颜色,就需要在theme中添加配置,如c-wine-red-453。

但是如果想任意使用50~950之间的任何深度颜色呢?在theme中把这近千种颜色都定义出来?🤔感觉不太合适,所以决定尝试创建一个预设库来实现下。
unocss-preset-magicolor
使用
bash
pnpm add unocss-preset-magicolor -D
与其他unocss预设库一样,需要在使用前将该预设添加到unocss配置文件中。
ts
import { defineConfig } from 'unocss';
import { presetMagicolor } from 'unocss-preset-magicolor';
export default defineConfig({ presets: [presetMagicolor()] });
基础用法
你可以使用50到950之间任何深度的任何颜色。如果你想添加一个新的主题颜色,也只需要在theme中配置默认颜色。颜色的不同深度会通过 magic-color生成,所有颜色格式都会转换为oklch类型.
ts
import { defineConfig, presetWind4 } from 'unocss';
import { presetMagicolor } from '../packages/presets/src';
export default defineConfig({
presets: [
presetWind4(),
presetMagicolor(),
],
theme: { colors: { wine: { red: '#9c1d1e' } } },
});
vue
<template>
<button class="px-8 py-4 bg-mc-rose-445 hover:bg-mc-wine-red-575">
<span class="c-mc-[#789411]-430">
Hello World!
</span>
</button>
</template>

使用class来定义颜色
现在,除了在theme中定义颜色,你还可以直接在class中定义颜色。它对于需要动态修改颜色的组件非常有用。
vue
<template>
<button class="px-8 py-4 mc-btn_[#9c1d1e] hover:mc-btn_blue bg-mc-btn-450">
<span class="c-mc-btn-610">
Hello World!
</span>
</button>
</template>

预设的实现逻辑
如果你对该预设的实现有兴趣,或者想了解下如何自定义unocss的预设,可以往下看看。
获取颜色更多的深度选择
unocss提供的调色板颜色,在50~950的11种深度颜色是固定的,没有什么特殊规律,也就无法从一个深度颜色值直接推算出其他的深度颜色值,所以这里如果想要获取red453这个深度的颜色值,思路就是先获取red400与red500的颜色值,53作为过渡比例进行计算,也就是red453 = red400 + (red500 - red400) * 0.53。
在计算前我们需要先获取到颜色,主题色我们可以通过rules中的Theme获取得到,但并不一定所有主题颜色都有50~950的这11个深度,所以这里我们使用大佬写的一个专门处理颜色的库Magic Color,这个库不仅可以转化颜色的格式,其中提供theme方法还可以直接生成任何颜色的11个不同深度颜色值,为后面实现自定义颜色奠定基础。
最后这里以color样式为例,先不考虑透明度这些样式,实现计算颜色深度的代码如下,其中使用mc则作为颜色的前缀,避免与其他预设冲突。
实现代码:
ts
import type { Preset, RuleContext } from 'unocss';
import type { PresetOptions } from './types';
import { mc } from 'magic-color';
import { parseColor, type Theme } from 'unocss/preset-mini';
export function presetMagicolor(_options?: PresetOptions): Preset<Theme> {
return {
name: 'unocss-preset-magicolor',
rules: [
[/^c-mc-(.+)$/, ([, str]: string[], ctx: RuleContext<Theme>) => {
const [color] = str.split(/[:/]/);
const depth = color.match(/.*-(\d+)/)?.[1];
const { theme } = ctx;
// 该颜色可直接解析获取到,直接返回
const parsedColor = parseColor(color, theme);
if (parsedColor?.color) {
return { color: parsedColor.color };
}
if (!depth) { return; }
const originColor = color.split(/-\d+-?/)[0];
if (!originColor || !Number.isNaN(Number(originColor))) { return; } // 无效的颜色
// 原深度值
const originDepth = Number(depth);
// 前深度值,不小于50
let beforeDepth = Math.floor(originDepth / 100) * 100;
beforeDepth = beforeDepth <= 50 ? 50 : beforeDepth;
// 后深度值,不大于950
let afterDepth = Math.floor((originDepth + 100) / 100) * 100;
afterDepth = afterDepth >= 950 ? 950 : afterDepth;
let beforeParsedColor = parseColor(`${originColor}-${beforeDepth}`, theme)?.color;
let afterParsedColor = parseColor(`${originColor}-${afterDepth}`, theme)?.color;
// 未能获取到对应深度的颜色时,通过mc.theme获取,用于自定义颜色
if (!beforeParsedColor || !afterParsedColor) {
const customColor = parseColor(originColor, theme)?.color || originColor;
if (!mc.valid(customColor)) { return; }
const themeColor = mc.theme(customColor);
if (!beforeParsedColor) {
beforeParsedColor = themeColor[beforeDepth];
}
if (!afterParsedColor) {
afterParsedColor = themeColor[afterDepth];
}
}
if (!mc.valid(beforeParsedColor) || !mc.valid(afterParsedColor)) { return; }
// 获取前后深度的HSL格式颜色
const beforeDepthColor = mc(beforeParsedColor).toHsl().values;
const afterDepthColor = mc(afterParsedColor).toHsl().values;
const transitionRatio = (originDepth - beforeDepth) / 100;
// 计算获得当前深度的颜色
const resultColor = Array.from({ length: 3 }).map((_, i) => {
const value = beforeDepthColor[i] + (afterDepthColor[i] - beforeDepthColor[i]) * transitionRatio;
return Math.round(value * 100) / 100;
});
return { color: `hsl(${resultColor[0]}deg ${resultColor[1]}% ${resultColor[2]}%)` };
}],
],
};
};
这时我们就可以给color使用50~950之间的任意深度颜色了。
vue
<template>
<div class="c-mc-blue-600">
Hello World!
</div>
<div class="c-mc-blue-620">
Hello World!
</div>
<div class="c-mc-blue-650">
Hello World!
</div>
<div class="c-mc-blue-680">
Hello World!
</div>
<div class="c-mc-blue-700">
Hello World!
</div>
</template>
并且当我们需要扩展更多颜色时,也仅需要在主题配置默认颜色,或者直接在class中自定义颜色即可,预设会通过代码中的mc.theme获取不同深度的颜色。
ts
import { defineConfig, presetMini } from 'unocss';
import { presetMagicolor } from '../packages/presets/src';
export default defineConfig({
presets: [presetMini(), presetMagicolor()],
theme: { colors: { wine: { red: '#990B0A'} }, // 仅需配置默认颜色
});
使用时颜色会自动获取对应的深度颜色,也可自定义颜色,且同时支持不同深度配置。
vue
<template>
<!-- 使用自定义的主题 -->
<div class="c-mc-wine-red-666">
Hello World!
</div>
<!-- 在自定义的颜色后面加深度即可 -->
<div class="c-mc-[#465422]-345">
Hello World!
</div>
</template>
更多样式支持
由于是自定义的预设规则,以上的代码也是只是作用于color属性,对于bg-color、border-color这些样式,我们同样也是需要手动添加规则去实现。
在样式规则上,我们以Wind4 preset版本的样式规则为主,可以通过引入其colorCSSGenerator方法来兼容c-op、bg-op这些功能,以达到最终的效果。
完整实现代码:https://github.com/nixwai/unocss-preset-magicolor/pull/1/files
vue
<template>
<div class="w-100vw h-100vh flex items-center justify-center">
<button class="px-8 py-4 bg-mc-blue-450 hover:bg-mc-blue-630 b-5 b-mc-yellow-860">
<span class="c-mc-[#798322]-420">
Hello World!
</span>
</button>
</div>
</template>

使用class给颜色定义名称
当前的预设已经满足我们在样式上任意使用颜色,但如果想要定义新颜色名依然只能在theme上定义,且并不支持动态更改该颜色,因为其深度的颜色是通过计算后直接赋值到对应的样式的。

因此,要想让颜色除了在theme上定义,这里想到的就是用特殊的前缀、名称、颜色值3者拼接的方式来定义一个class,之后在其元素以及子元素便可使用该颜色名代替颜色,格式为mc-颜色名_颜色值,类似于js的变量定义:var 颜色名=颜色值,只不过这里我们的mc代替了var,_代替了=,且定义后还可以通过切换class的颜色值方式来动态切换所有使用该颜色名的颜色。如:
vue
<template>
<div class="w-100vw h-100vh flex items-center justify-center">
<button class="px-8 py-4 mc-btn_[#104CB3] hover:mc-btn_[#0B3782] bg-mc-btn-450">
<span class="c-mc-btn-120">
Hello World!
</span>
</button>
</div>
</template>
但是要实现这个功能并不容易,在unocss自定义rules时,每个rules的运行都是独立的,意味着使用mc-btn_[#104CB3]定义的颜色,在rules中的bg-mc-btn与c-mt-btn无法直接获取到btn具体的颜色值,因此这里需要使用的css变量来保存定义的颜色,使用时再通过css变量间接获取到颜色。例如:
css
.mc-btn_[#104CB3] {
--mc-btn-color: #104CB3;
}
.bg-mc-btn {
background: var(--mc-btn-color);
}
.c-mc-btn {
color: var(--mc-btn-color);
}
实现代码:
ts
import type { Preset } from 'unocss';
import type { PresetOptions } from './types';
import { parseColor } from '@unocss/preset-wind4/utils';
export function presetMagicolor(_options?: PresetOptions): Preset {
return {
name: 'unocss-preset-magicolor',
rules: [
[/^mc-(.+)$/, ([, str], { theme }) => {
const [name, hue] = str.split('_');
const color = hue?.split(/[:/]/)?.[0];
if (!color) {
return {};
}
const parsedColor = parseColor(color, theme);
if (!parsedColor?.color) {
return {};
}
return { [`--mc-${name}-color`]: parsedColor.color };
}],
[/^c-mc-(.+)$/, ([, str]) => {
const [bodyColor] = str.split(/[:/]/);
return { color: `var(--mc-${bodyColor}-color)` };
}],
[/^bg-mc-(.+)$/, ([, str]) => {
const [bodyColor] = str.split(/[:/]/);
return { background: `var(--mc-${bodyColor}-color)` };
}],
],
};
};
当然这只是单一颜色的获取,定义后也只能使用对应名称的颜色,要想在使用时也能随意获取到不同深度的颜色,除了需要保存定义的颜色,还要获取颜色的那11个深度颜色值并保存下来,之后使用时便可以利用公式计算出来。如:
css
.mc-wine-red_[#9c1d1e] {
--mc-wine-red-color: #9c1d1e;
--mc-wine-red-50-l: 0.971;
--mc-wine-red-50-c: 0.013;
--mc-wine-red-50-h: 17.38;
--mc-wine-red-100-l: 0.936;
--mc-wine-red-100-c: 0.031;
--mc-wine-red-100-h: 17.717;
--mc-wine-red-200-l: 0.886;
--mc-wine-red-200-c: 0.057;
--mc....
--mc-wine-red-950-l: 0.257;
--mc-wine-red-950-c: 0.086;
--mc-wine-red-950-h: 25.723;
}
.bg-wine-red-450 {
background-color: oklch(var(--mc-wine-red-450-l) var(--mc-wine-red-450-c) var(--mc-wine-red-450-h));
--mc-wine-red-450-l: calc(var(--mc-wine-red-400-l) + 0.5 * (var(--mc-wine-red-500-l) - var(--mc-wine-red-400-l)));
--mc-wine-red-450-c: calc(var(--mc-wine-red-400-c) + 0.5 * (var(--mc-wine-red-500-c) - var(--mc-wine-red-400-c)));
--mc-wine-red-450-h: calc(var(--mc-wine-red-400-h) + 0.5 * (var(--mc-wine-red-500-h) - var(--mc-wine-red-400-h)));
}
.c-wine-red-600 {
color: oklch(var(--mc-wine-red-600-l) var(--mc-wine-red-600-c) var(--mc-wine-red-600-h));
}
完整实现代码:https://github.com/nixwai/unocss-preset-magicolor/pull/2/files
更多功能
目前预设的开发基本完成,但可能还有些问题需要在后续优化,比如在使用class定义颜色后会产生数十个css变量,但并不是每个变量都会使用到,目前想到的解决办法是将他们合并成一个,再通过模运算拆分出来,以减少css变量。如:
css
// 拼接起来,每个值占用数字的6位
.mc-wine-red_[#9c1d1e] {
--mc-wine-red-color: #9c1d1e;
--mc-wine-red-50-200-l: 882000932000.97;
--mc-wine-red-300-500-l: 623000707000.809;
--mc-wine-red-600-800-l: 199000488000.546;
--mc-wind-red-900-950-l: 282000.379;
--mc-wine-red-50-200-c: ...
--mc-wine-red-300-500-c: ...
...
}
// 使用时通过模运算取值
.bg-wine-red-450 {
background-color: oklch(var(--mc-wine-red-450-l) var(--mc-wine-red-450-c) var(--mc-wine-red-450-h));
--mc-wine-red-450-l: calc(
mod(var(--mc-wine-red-300-500-l), 1000000000) / 1000000 +
0.5 * (
mod(var(--mc-wine-red-300-500-l), 1000000000000000) / 1000000000000 -
mod(var(--mc-wine-red-300-500-l), 1000000000) / 1000000
)
);
...
}
虽然css变量减少了,但在计算复杂了不少,并不是个特别好的解决办法,所以并未实现出来,如果你有更好的解决办法或者思路,欢迎评论提出来。
如果你喜欢这个预设,欢迎star,也欢迎提出你的看法。