mcpField
C
color-palette

color-palette

github.com/jezweb设计与 UI1.1k3.3k 次安装3 周前更新
代码仓库

关于 color-palette

此技能将单个品牌十六进制颜色转换为 Tailwind v4 色彩系统,并映射语义令牌及明暗模式。还涵盖对比度检查和无障碍调整。

主要功能

  1. 01生成 11 阶色阶
  2. 02将色阶映射到语义令牌
  3. 03定义明暗模式的配色对
  4. 04检查 WCAG 对比度
  5. 05输出 Tailwind v4 CSS

适用场景

  1. 01根据品牌色创建主题
  2. 02设置成对的背景与前景令牌
  3. 03检查两种模式下的文本和界面对比度

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/jezweb/claude-skills --skill color-palette

安装后在 Claude Code 中输入/color-palette确认已加载。

附带文件assets/tailwind-colors.css

详细介绍

色板生成器

从单个品牌色十六进制值生成完整、无障碍的色彩系统。生成可直接粘贴到项目中的 Tailwind v4 CSS。

工作流

步骤 1:获取品牌色十六进制值

询问主品牌色。只需提供一个十六进制值,例如 #0D9488。

步骤 2:生成 11 阶色阶

将十六进制值转换为 HSL,然后在保持色相不变的同时调整亮度,生成不同深浅的颜色。

十六进制转 HSL
function hexToHSL(hex) {
  hex = hex.replace(/^#/, '');
  const r = parseInt(hex.substring(0, 2), 16) / 255;
  const g = parseInt(hex.substring(2, 4), 16) / 255;
  const b = parseInt(hex.substring(4, 6), 16) / 255;

  const max = Math.max(r, g, b);
  const min = Math.min(r, g, b);
  const diff = max - min;

  let l = (max + min) / 2;
  let s = 0;
  if (diff !== 0) {
    s = l > 0.5 ? diff / (2 - max - min) : diff / (max + min);
  }

  let h = 0;
  if (diff !== 0) {
    if (max === r) h = ((g - b) / diff + (g < b ? 6 : 0)) / 6;
    else if (max === g) h = ((b - r) / diff + 2) / 6;
    else h = ((r - g) / diff + 4) / 6;
  }

  return { h: Math.round(h * 360), s: Math.round(s * 100), l: Math.round(l * 100) };
}
亮度和饱和度值
色阶亮度饱和度乘数使用场景
5097%0.80低调的背景
10094%0.80悬停状态
20087%0.85边框、分隔线
30075%0.90禁用状态
40062%0.95占位文本
50048%1.00品牌色基准
60040%1.00主要操作(通常为品牌色)
70033%1.00主按钮悬停状态
80027%1.00激活状态
90020%1.00浅色背景上的文本
95010%1.00最深的强调色

降低浅色阶的饱和度(50-200 降低 15-20%,300-400 降低 5-10%),避免生成过于鲜艳的粉彩色。500-950 保持完整饱和度。

完整色阶生成器
function generateShadeScale(brandHex) {
  const { h, s } = hexToHSL(brandHex);
  const shades = {
    50:  { l: 97, sMul: 0.8 },  100: { l: 94, sMul: 0.8 },
    200: { l: 87, sMul: 0.85 }, 300: { l: 75, sMul: 0.9 },
    400: { l: 62, sMul: 0.95 }, 500: { l: 48, sMul: 1.0 },
    600: { l: 40, sMul: 1.0 },  700: { l: 33, sMul: 1.0 },
    800: { l: 27, sMul: 1.0 },  900: { l: 20, sMul: 1.0 },
    950: { l: 10, sMul: 1.0 }
  };
  const result = {};
  for (const [shade, { l, sMul }] of Object.entries(shades)) {
    result[shade] = `hsl(${h}, ${Math.round(s * sMul)}%, ${l}%)`;
  }
  return result;
}
HSL 转十六进制
function hslToHex(h, s, l) {
  s = s / 100; l = l / 100;
  const c = (1 - Math.abs(2 * l - 1)) * s;
  const x = c * (1 - Math.abs((h / 60) % 2 - 1));
  const m = l - c / 2;
  let r = 0, g = 0, b = 0;
  if (h < 60) { r = c; g = x; }
  else if (h < 120) { r = x; g = c; }
  else if (h < 180) { g = c; b = x; }
  else if (h < 240) { g = x; b = c; }
  else if (h < 300) { r = x; b = c; }
  else { r = c; b = x; }
  r = Math.round((r + m) * 255);
  g = Math.round((g + m) * 255);
  b = Math.round((b + m) * 255);
  return `#${r.toString(16).padStart(2, '0')}${g.toString(16).padStart(2, '0')}${b.toString(16).padStart(2, '0')}`.toUpperCase();
}
验证

生成的色阶应属于同一色系,并呈现平滑的渐变。浅色阶(50-300)可用于背景,深色阶(700-950)可用于文本。500-700 应能辨认出品牌色。


步骤 3:映射语义令牌

每个背景令牌都 MUST 配有对应的前景令牌。切勿单独使用背景,否则深色模式会出问题。

浅色模式令牌
令牌色阶使用场景
background白色页面背景
foreground950正文
card白色卡片背景
card-foreground900卡片文本
popover白色下拉菜单/工具提示背景
popover-foreground950下拉菜单文本
primary600主按钮、链接
primary-foreground白色主按钮上的文本
secondary100次要按钮
secondary-foreground900次要按钮上的文本
muted50禁用状态背景、低调的区块
muted-foreground600弱化文本、说明文字
accent100悬停状态、低调的高亮
accent-foreground900强调色背景上的文本
destructive红色-600删除按钮、错误
destructive-foreground白色危险操作按钮上的文本
border200输入框边框、分隔线
input200输入字段边框
ring600焦点环
深色模式令牌
令牌色阶使用场景
background950页面背景
foreground50正文
card900卡片背景
card-foreground50卡片文本
popover900下拉菜单背景
popover-foreground50下拉菜单文本
primary500主按钮(深色模式下更明亮)
primary-foreground白色主按钮上的文本
secondary800次要按钮
secondary-foreground50次要按钮上的文本
muted800禁用状态背景
muted-foreground400弱化文本
accent800悬停状态
accent-foreground50强调色背景上的文本
destructive红色-500删除按钮(更明亮)
destructive-foreground白色危险操作上的文本
border800边框
input800输入框边框
ring500焦点环
深色模式反转模式

深色模式会反转明度,同时保留色相和饱和度。交换两端的色阶(50 变为 950,950 变为 50),中间色阶保持不变(500 仍接近 500)。

浅色模式色阶深色模式对应色阶用途
50950背景
100900微妙背景
200800边框
500500(稍亮)品牌基准色
600400主要操作
95050文本颜色

深色模式的关键原则:

  • 主要颜色使用色阶 500(而不是 600)——在深色背景上更亮、更醒目
  • 文本使用色阶 50(米白色),而不是纯 #FFFFFF——更护眼
  • 边框需要比背景亮约 10-15%(例如,在 950 背景上使用 800 边框)
  • 海拔越高,颜色越亮(与浅色模式中的阴影相反)
  • 更改背景时,始终同时更新前景

第 4 步:检查对比度
WCAG 最低对比度
内容类型AAAAA
正常文本(<18px 或 <14px 粗体)4.5:17:1
大号文本(>=18px 或 >=14px 粗体)3:14.5:1
UI 组件(按钮、边框)3:1未定义
图形对象(图标、图表)3:1未定义

大多数项目以 AA 为目标;对无障碍要求较高的场景(政府、医疗)以 AAA 为目标。

亮度与对比度公式
function getLuminance(hex) {
  hex = hex.replace(/^#/, '');
  const r = parseInt(hex.substring(0, 2), 16) / 255;
  const g = parseInt(hex.substring(2, 4), 16) / 255;
  const b = parseInt(hex.substring(4, 6), 16) / 255;
  const rsRGB = r <= 0.03928 ? r / 12.92 : Math.pow((r + 0.055) / 1.055, 2.4);
  const gsRGB = g <= 0.03928 ? g / 12.92 : Math.pow((g + 0.055) / 1.055, 2.4);
  const bsRGB = b <= 0.03928 ? b / 12.92 : Math.pow((b + 0.055) / 1.055, 2.4);
  return 0.2126 * rsRGB + 0.7152 * gsRGB + 0.0722 * bsRGB;
}

function getContrastRatio(hex1, hex2) {
  const lum1 = getLuminance(hex1);
  const lum2 = getLuminance(hex2);
  const lighter = Math.max(lum1, lum2);
  const darker = Math.min(lum1, lum2);
  return (lighter + 0.05) / (darker + 0.05);
}
快速检查表 —— 浅色模式
前景背景比率是否通过?使用场景
950白色18.5:1AAA正文
900白色14.2:1AAA卡片文本
700白色8.1:1AAA文本
600白色5.7:1AA文本、按钮
500白色3.9:1未通过作为文本时太浅
白色6005.7:1AA按钮文本
白色7008.1:1AAA按钮文本
600505.4:1AA弱化区块文本
快速检查表 —— 深色模式
前景背景比率是否通过?使用场景
5095018.5:1AAA正文
5090014.2:1AAA卡片文本
4009508.2:1AAA弱化文本
4009006.3:1AA弱化文本
白色6005.7:1AA按钮文本

经验法则:对于文本,前景与背景的明度差应达到 50% 或以上。

需要验证的关键组合
  1. 正文:背景上的前景(浅色模式:白色上的 950 = 18.5:1,深色模式:950 上的 50 = 18.5:1)
  2. 主要按钮:主要前景色叠加在主要颜色上(浅色模式:600 上的白色 = 5.7:1,深色模式:500 上的白色 = 3.9:1——处于临界值)
  3. 弱化文本:弱化前景色叠加在弱化颜色上(浅色模式:50 上的 600 = 5.4:1,深色模式:800 上的 400 = 4.1:1——可能不达标)
  4. 卡片文本:卡片前景色叠加在卡片上(浅色模式:白色上的 900 = 14.2:1,深色模式:900 上的 50 = 14.2:1)
修复常见的对比度问题

白色文本叠加在 primary-500 上未达标(3.9:1):改用 primary-600(5.7:1),或在按钮上使用深色文本。

深色模式中的弱化文本未达标(800 上的 400 = 4.1:1):改用 900 上的 300 = 6.8:1。

链接不够醒目(白色上的 500 = 3.9:1):改用 primary-700(8.1:1),或添加下划线装饰。


第 5 步:输出 Tailwind v4 CSS
@import "tailwindcss";

@theme {
  /* Shade scale */
  --color-primary-50: #F0FDFA;
  --color-primary-100: #CCFBF1;
  --color-primary-200: #99F6E4;
  --color-primary-300: #5EEAD4;
  --color-primary-400: #2DD4BF;
  --color-primary-500: #14B8A6;
  --color-primary-600: #0D9488;
  --color-primary-700: #0F766E;
  --color-primary-800: #115E59;
  --color-primary-900: #134E4A;
  --color-primary-950: #042F2E;

  /* Light mode semantic tokens */
  --color-background: #FFFFFF;
  --color-foreground: var(--color-primary-950);
  --color-card: #FFFFFF;
  --color-card-foreground: var(--color-primary-900);
  --color-popover: #FFFFFF;
  --color-popover-foreground: var(--color-primary-950);
  --color-primary: var(--color-primary-600);
  --color-primary-foreground: #FFFFFF;
  --color-secondary: var(--color-primary-100);
  --color-secondary-foreground: var(--color-primary-900);
  --color-muted: var(--color-primary-50);
  --color-muted-foreground: var(--color-primary-600);
  --color-accent: var(--color-primary-100);
  --color-accent-foreground: var(--color-primary-900);
  --color-destructive: #DC2626;
  --color-destructive-foreground: #FFFFFF;
  --color-border: var(--color-primary-200);
  --color-input: var(--color-primary-200);
  --color-ring: var(--color-primary-600);
  --radius: 0.5rem;
}

/* Dark mode overrides */
.dark {
  --color-background: var(--color-primary-950);
  --color-foreground: var(--color-primary-50);
  --color-card: var(--color-primary-900);
  --color-card-foreground: var(--color-primary-50);
  --color-popover: var(--color-primary-900);
  --color-popover-foreground: var(--color-primary-50);
  --color-primary: var(--color-primary-500);
  --color-primary-foreground: #FFFFFF;
  --color-secondary: var(--color-primary-800);
  --color-secondary-foreground: var(--color-primary-50);
  --color-muted: var(--color-primary-800);
  --color-muted-foreground: var(--color-primary-400);
  --color-accent: var(--color-primary-800);
  --color-accent-foreground: var(--color-primary-50);
  --color-destructive: #EF4444;
  --color-destructive-foreground: #FFFFFF;
  --color-border: var(--color-primary-800);
  --color-input: var(--color-primary-800);
  --color-ring: var(--color-primary-500);
}

将 assets/tailwind-colors.css 复制为起始模板。


组件用法示例

// Primary button
<button className="bg-primary text-primary-foreground hover:bg-primary/90">Click me</button>

// Secondary button
<button className="bg-secondary text-secondary-foreground hover:bg-secondary/80">Cancel</button>

// Card
<div className="bg-card text-card-foreground border-border rounded-lg">
  <h2>Title</h2>
  <p className="text-muted-foreground">Description</p>
</div>

// Input
<input className="bg-background text-foreground border-input focus:ring-ring" />

常见调整

  • 浅色阶过于鲜艳:将饱和度降低 10-20%
  • 主要颜色的对比度不佳:文本使用色阶 700 或更高
  • 深色模式太暗:背景使用色阶 900,而不是 950
  • 品牌颜色太浅或太深:调整到色阶 500-600 范围
  • 深色模式看起来发灰:主要颜色使用色阶 500(比浅色模式的 600 更亮)
  • 深色模式中纯白文本过于刺眼:改用色阶 50(米白色)
  • 深色模式中的弱化文本对比度未达标:使用差异更大的色阶(在 900 上使用 300,而不是在 800 上使用 400)
品牌形象调整
  • 保守型品牌(金融、法律):按钮使用 primary-700,降低浅色的饱和度
  • 活力型品牌(创意、科技):使用 primary-500-600,保持完整饱和度
  • 极简型品牌(设计、建筑):谨慎使用 primary,突出柔和色调和细腻的边框(primary-100)

验证清单

  • 正文:>=4.5:1(普通文本)或 >=3:1(大号文本)
  • 主按钮文本:>=4.5:1
  • 次按钮文本:>=4.5:1
  • 柔和文本:>=4.5:1
  • 链接:>=4.5:1(或添加下划线)
  • UI 元素(边框):>=3:1
  • 焦点指示器:>=3:1
  • 错误文本:>=4.5:1
  • 深色模式:通过上述所有检查
  • 每种背景都有对应的前景色
  • 品牌色在两种模式下都易于辨认
  • 边框清晰可见但不过于刺眼
  • 卡片/区块的边界清晰

发布前请测试两种模式。


可选参考资料

  • 在线对比度检查工具:WebAIM (webaim.org/resources/contrastchecker)、Coolors (coolors.co/contrast-checker)、Accessible Colors (accessible-colors.com)
  • CI/CD 对比度测试:在测试套件中使用 getContrastRatio(),断言所有令牌对都达到最低比率
  • 透明度/渐变边缘情况:对于带透明度的颜色,根据最终渲染颜色计算。对于渐变,检查两个端点。
  • OLED 深色模式:使用 @media (prefers-contrast: high) 和 #000000 背景,以节省 AMOLED 屏幕的电量
  • 多色调色板:为每种品牌色生成单独的色阶,并映射到不同的语义角色(主色、强调色)
  • 调色板可视化工具:coolors.co、paletton.com、Figma 色板
  • assets/tailwind-colors.css — 完整的 CSS 输出模板

评论

支持 Markdown