
做前端样式、做设计稿、改软件主题配色或者只是想在文档里把颜色说清楚早晚都会撞上颜色十六进制代码对照表这六个字。它看起来像一张查表用的速查卡实际上它背后连着三件事颜色的数值表示、不同软件对同一串代码的解析顺序、以及屏幕和纸张之间那道永远消不掉的颜色误差。我最早接触这套东西是在做一个后台管理系统的主题换肤功能当时把一个 #3C8DBC 填进配置里界面上出现的却是偏紫的颜色排查了大半天才发现是通道顺序的问题。这篇内容就把我这些年攒下来的对照经验、换算手法和踩过的坑整理一遍适合刚入行的前端、需要批量处理配色的设计师也适合偶尔要改一改老软件配色却不知道从哪下手的人。1. 六位字符背后的三组数先把十六进制颜色的账算清楚1.1 为什么颜色要用十六进制来记颜色在计算机里最原始的形态是三个整数分别代表红、绿、蓝三个通道的强度每个通道的取值范围是 0 到 255。这个设计不是随便定的一个字节正好 8 位能表示 256 个状态三个字节凑成 24 位理论上能拼出 16777216 种颜色。如果直接用十进制写红色就是rgb(255,0,0)写起来不算长但在需要把颜色嵌进 CSS 属性、配置文件、二进制结构或者 URL 参数里的时候rgb()这种带括号和逗号的形式会有很多麻烦尤其是它没法直接参与字符串拼接和位运算。十六进制解决的正是这个问题。十六进制的 16 个数字符号是 0 到 9 加上 A 到 F一个十六进制位正好对应 4 个二进制位两个十六进制位正好对应 8 个二进制位也就是一个字节。于是 0 到 255 这个范围恰好可以用两个十六进制字符完整覆盖最小是00最大是FF。三个通道各出两个字符拼起来就是六位前面加个#作为标记这也就是#RRGGBB的由来。这个结构的优雅之处在于它和内存里的字节布局是一一对应的。你看到#3C8DBC其实就等于看到了三个字节 0x3C、0x8D、0xBC 按顺序排在那里。理解了这一层后面遇到 BGR 顺序、alpha 通道、字节序这些问题时就不会觉得莫名其妙因为它们本质上都是在问同一件事这三个字节到底按什么顺序摆。1.2 #RRGGBB 每一位的权重和手算方法很多人用拾色器用惯了离开工具就不知道怎么算其实转换只需要记住两条除以 16 取整得到高位取余得到低位。拿一个真实例子来走一遍假设目标色是 rgb(200, 141, 188)。200 除以 16 等于 12余 812 对应十六进制的 C所以 200 写成C8。141 除以 16 等于 8余 1313 对应 D所以 141 写成8D。188 除以 16 等于 11余 1211 对应 B12 对应 C所以 188 写成BC。拼起来就是#C88DBC一个带点灰调的藕粉色。反过来从十六进制还原十进制也一样简单把每一位乘上它的权重再相加。还是拿C8举例高位 C 代表 12权重是 16低位 8 权重是 1结果就是 12×16 8 200。三位十六进制字符的权重依次是 256、16、1熟练之后心算几秒就能出结果。这里有个练习方法值得推荐拿几个常见颜色把十进制和十六进制来回转换各做十遍。白色的#FFFFFF是 (255,255,255)黑色的#000000是 (0,0,0)纯红的#FF0000是 (255,0,0)纯蓝的#0000FF是 (0,0,255)。多算几次之后你会形成直觉看到80就知道大概是中间值 128看到CC就知道是 204 上下这在调色的时候特别有用因为渐变的关键点常常就落在这几个整数值上。1.3 三位简写的合并规则与不该用它的场合CSS 允许把六位简写成三位规则是把每一位重复一次。#ABC展开后是#AABBCC#F00展开后是#FF0000。这个简写能省下三个字符在手工写样式的时候确实方便但它有一个硬性前提每一对通道的两个字符必须完全相同否则简写是不合法的写法。#3C8DBC就不能简写成#38C因为 0x3C 和 0x33 差了很远。更需要注意的是简写只在 CSS 这类明确支持它的语法里有效。如果把这套简写习惯带到配置文件、程序代码或者十六进制编辑器里大概率会出问题。我见过有人在一款老软件的配置文件里填了#F00软件解析时按固定长度读取读到的是#F00后面跟的别的字段结果整个配置项错位颜色显示成一片纯白。稳妥的做法是凡是写进非 CSS 场景的颜色值一律补全成六位只有在纯样式表里、且确定维护者都懂这套规则时才考虑简写。顺带说一个相关的细节CSS 里颜色不区分大小写#ff0000和#FF0000等价。但有些程序在做字符串比较或者去重的时候是区分大小写的同一个颜色被记成两条记录。做配色管理的时候统一成大写是个好习惯至少在肉眼比对时不容易看花眼。2. 一份真正能用的对照表该收哪些颜色2.1 基础色与 Web 安全色的边界市面上流传的颜色十六进制代码对照表大多有两个毛病要么只列十几种基础色实际做项目根本不够用要么一口气堆几百行看着壮观但没法查。我的建议是按使用频率分层来组织。第一层是十几个必须背下来的基础色包括白#FFFFFF、黑#000000、红#FF0000、绿#00FF00、蓝#0000FF、黄#FFFF00、青#00FFFF、洋红#FF00FF以及中间灰#808080。这九到十个是换算的锚点剩下所有颜色都可以通过它们的比例关系去理解。第二层值得知道的是 Web 安全色。这套色板的历史原因是在早期显卡只能显示 256 色的年代为了确保颜色在不同设备上不出现抖动把每个通道限定在 00、33、66、99、CC、FF 这六个值上三个通道组合起来一共 216 色。这个限制在今天已经基本没有意义了现代显示器都是 24 位真彩但这套色板的取值规律仍然有参考价值它本质上是一套均匀分布的色阶。通道值十进制说明000最暗335120% 档6610240% 档9915360% 档CC20480% 档FF255最亮如果你的项目还在支持一些很老的设备或者嵌入式屏幕这套色板仍然是最保险的选择因为它们在色彩量化时不会产生突变。日常项目则不必拘泥直接用拾色器取想要的色值就行。2.2 灰阶用得最多却最容易被漏掉的一段界面里出现频率最高的其实不是彩色而是灰色。文字、边框、分割线、禁用状态、阴影全是灰。可很多对照表里灰色只有三行白、灰、黑这就很不实用。我一般会把灰阶单独拉出来做成一条完整的刻度从接近纯白一直排到接近纯黑。十六进制RGB常见用途#FFFFFF255,255,255卡片底色、反白文字#FAFAFA250,250,250页面背景#F2F2F2242,242,242区块分隔底#E6E6E6230,230,230分割线#CCCCCC204,204,204输入框边框#999999153,153,153次要文字、占位符#666666102,102,102正文次级文字#33333351,51,51标题、主文字#1A1A1A26,26,26深色主题底#0000000,0,0纯黑慎用于大面积这条刻度有个很实用的性质相邻两档之间的差值基本相等视觉上过渡自然。如果你自己设计一套灰阶别凭感觉随便挑按等间距取做出来的界面会稳很多。另外深色模式下的灰阶不是简单反转通常要把深色的对比度压低一些否则在暗环境下会刺眼。2.3 传统色和网红色号的对应关系最近几年传统色卡和所谓的网红色号话题很热很多人拿着一张色卡到处找十六进制值。这里要提醒一句传统色的命名体系本身是描述性的不是数值标准同一个月白在不同版本的色卡里可以从#EEF7F2排到#D6ECF0跨度不小。所以看到两份资料给的值不一样别急着判定哪份错了先看它引用的是哪套色卡。陶土白这类名称更是如此它带有很强的商业色彩最初出现在消费品命名里同一个名字下不同批次的产品实际颜色都会有偏差。真要拿来做设计正确流程是拿到实物图片或者样品用拾色器取色而不是去搜一个标准值。取色时还要注意光源同一件物品在日光下和室内灯光下拍出来的照片色值能差出二三十个数值单位。名称常见流传值使用建议克莱因蓝#002FA7数值相对稳定不同资料差异小勃艮第红#800020偏深的酒红常被误写成 #900020蒂芙尼蓝#0ABAB5 / #81D8D0存在两个流传版本谨慎直接引用爱马仕橙#F37021与实物仍有差距建议现场取色莫兰迪灰无固定值是一类低饱和色的统称不存在唯一色值说白了对照表能给你的是参考和起点不能给你正确答案。任何声称某个名字对应唯一色值的说法都值得打个问号。3. 补零还是补一转换环节里最容易写错的那一步3.1 十进制转十六进制的位数补齐关于十六进制补1这个说法网上流传的版本不少但真正的规则其实是补零不是补一。原因很简单十六进制是位置记数法往高位补零不改变数值往高位补一就改变数值了。比如十进制 5 转成十六进制是 5只有一个字符但要放进两位的通道里必须写成0505和5是同一个数而15就变成 21 了完全是另一个数。这条规则在写格式化代码时最容易翻车。下面这个写法就是错的# 错误写法小数值会丢掉前导零 rgb (5, 141, 188) hex_str # hex(rgb[0])[2:] hex(rgb[1])[2:] hex(rgb[2])[2:] # 结果是 #58dbc位数都不对了正确做法是用格式化字符串强制补零到两位def rgb_to_hex(r, g, b): return #{:02X}{:02X}{:02X}.format(r, g, b) print(rgb_to_hex(5, 141, 188)) # #058DBC print(rgb_to_hex(255, 0, 0)) # #FF0000{:02X}里的02就是不足两位补零X表示用大写字母输出。这个小细节在很多手写的转换工具里都会被忽略导致深色系颜色转换出来位数不对然后整串颜色数据错位。判断方法很直接转换完之后检查长度是不是 6不是就说明有通道没补零。3.2 八位颜色与透明度通道当颜色需要带透明度时十六进制会扩展到八位多出来的两个字符表示 alpha 通道00 是完全透明FF 是完全不透明。问题在于不同平台把这两个字符放在的位置不一样这是我最常遇到的坑之一。平台或语法排列顺序举例不透明的橙红CSS Color 4#RRGGBBAA#E2725BFFAndroid 资源#AARRGGBB#FFE2725B部分图形库#RRGGBBAA#E2725BFF部分老式框架高字节补 FF0xFFE2725B从表里能看出来Andriod 系的写法和 CSS 系正好头尾颠倒。把#FFE2725B直接扔进 CSS浏览器会把它理解成红色 255、绿色 226、蓝色 114、透明度 91出来的是一条很淡的粉紫色和橙色毫无关系。反过来把 CSS 的八位色值放进 Android 资源里颜色也会跑偏。有没有办法快速区分看第一个通道的数值是不是 FF 或者 00 这种极端值。如果一段八位色值的前两位是 FF那它大概率是 Android 写法里的 alpha也可能是 CSS 写法里一个接近满红的颜色。这时候最稳的办法是查一遍目标平台的文档别靠猜。3.3 负值、符号位和 FFFFFFFF还有一种补位是补 F也就是符号扩展这个跟颜色格式没直接关系但在处理颜色数值时经常连带出现。在 Java 这类语言里Color.getRGB()返回的是一个 int而 32 位 int 是有符号的。当颜色是纯白0xFFFFFFFF时作为有符号整数它等于 -1。同样0xFF000000作为有符号整数等于 -16777216。这不是 bug是补码表示的正常结果。为什么颜色值会补成八个 F因为很多语言在内部把颜色统一存成 32 位整数高位那个字节专门用来放 alpha 或者保留位。当 alpha 是 FF 时高位就是 FF。如果把这个 int 直接输出成十六进制字符串就会得到ffffffff这样的八位结果而不是你期待的六位ffffff。处理这类问题的标准做法是做位运算截取# 从 32 位整数里取出 RGB 三个通道 def int_to_hex(value): # 只保留低 24 位屏蔽掉高位 value value 0xFFFFFF return #{:06X}.format(value) print(int_to_hex(-1)) # #FFFFFF print(int_to_hex(0xFF3C8DBC)) # #3C8DBC 0xFFFFFF这一步是关键它把高八位清掉只留下 RGB 部分。如果你在对接第三方接口时发现返回的颜色值是负数或者八位字符八成就是这个问题先做一次掩码运算再往下走。4. 红蓝颠倒RGB 与 BGR 的顺序差是怎么来的4.1 Windows 的 COLORREF 为什么是 BGR在 Windows 的图形接口里颜色常量叫 COLORREF它是一个 32 位整数但通道的排列方式和网页里完全不同实际格式是0x00BBGGRR。也就是说最低的那个字节是红色接着是绿色最高字节放蓝色。为什么这么设计因为 COLORREF 是从早期的 16 位系统继承下来的当年为了兼容 8 位色时代的调色板索引方式把红绿蓝按这个顺序排列后来就一直沿用成了事实标准。这个顺序带来的直接后果是如果有人把一个 Windows 的颜色数值误当成网页色值来理解红和蓝会完全对调。0x000000FF在 Windows 语义里是纯红REDFF但如果你按0x00RRGGBB去读会以为它是纯蓝。这就是我开头说的那次排查的根源。换算关系其实不难记只需要把 RRGGBB 整段倒过来取字节顺序然后在高位补一个 00。反过来也一样。红色#FF0000对应 COLORREF 的0x000000FF十进制是 255这也是为什么很多老软件里红色这个常量的值就是 255看着特别反直觉。4.2 行情终端与老式软件里怎么填颜色不少行情终端、老式报表软件和工业组态软件都允许用户自定义界面配色它们内部用的就是 COLORREF 那套规则。有的软件在配置界面里让你直接填十六进制这时候格式标注往往是COLORRRGGBB或者COLORBBGGRR差一个字母结果完全相反。我见过有人把网页上查到的#3C8DBC直接粘进去得到了一个偏红紫的颜色就是因为软件要的是 BGR 排列。正确的做法是先把目标色拆成三个通道然后按软件规定的顺序重新排。拿#3C8DBC举例R3C、G8D、BBC如果软件要求 RGB 顺序填3C8DBC。如果软件要求 BGR 顺序填BC8D3C。如果软件要求填十进制先按 BGR 排出0xBC8D3C换算成十进制是 12356924。换算步骤最好用脚本做一遍别手算尤其是需要批量替换十几处配色的时候。填完之后一定要打开界面确认一下最保险的验证办法是分别填入纯红FF0000和纯蓝0000FF各看一眼红蓝位置对了说明顺序就对了。这个两步验证法花不了十秒能省下大量返工时间。4.3 用十六进制编辑器定位颜色字节的完整过程有些软件的配色写死在可执行文件或者资源文件里界面上没有入口这时候就需要动用十六进制编辑器。这类工具能直接查看和修改文件的二进制内容常用来处理主题定制、资源汉化这类需求。整个流程我梳理成下面这几步。第一步备份。这一步没有任何商量余地改坏了要有原文件可以还原。复制一份到别的目录最好再加个日期后缀。第二步确定目标颜色。用拾色器取到想要的色值比如#E2725B拆成三个字节 0xE2、0x72、0x5B。第三步构造搜索序列。因为颜色在文件里可能以 RGB 顺序存也可能以 BGR 顺序存还可能带一个 alpha 字节所以要准备几组候选RGBE2 72 5BBGR5B 72 E2RGBAE2 72 5B FFBGRA5B 72 E2 FF第四步用十六进制编辑器的查找十六进制字节功能逐个搜索。如果搜到的位置附近还有一组类似结构的字节那基本就能确认是一张颜色表。第五步替换并保存然后运行程序验证。如果颜色没变可能是文件被缓存了清一下配置或者换个用户目录再试。这里有个经验值得说修改可执行文件里的资源之前先确认程序有没有做完整性校验。有些软件启动时会校验自身文件改过之后会直接报错退出。这种情况就没法用编辑器改只能从配置文件入手或者干脆放弃。另外字节搜索失败不一定代表没有颜色数据也可能是颜色被压缩存储或者做了异或加密这时候搜索原始字节就找不到了需要换个思路。5. 取色落地后的误差截图、屏幕与打印5.1 显示器色域和缩放带来的偏差同一个色值#E2725B在广色域显示器和普通 sRGB 显示器上看观感是不一样的。广色域屏幕能显示更饱和的红同一个数值在它上面会显得更艳。这不是显示错误是屏幕物理特性决定的。做设计交付时如果客户端和你自己的屏幕色域差很多来回改稿会非常痛苦。比较务实的做法是把交付文件里标注色值的空间通常默认按 sRGB 处理同时在说明里写清楚这一点。还有一个容易被忽略的因素是系统缩放。在缩放比例不是 100% 的屏幕上有些截图工具会把画面重新采样边缘像素被插值混合取到的颜色就不是原始值了。我遇到过取一个纯色块的色值得到的结果比预期偏了七八个数值单位原因就是缩放导致的插值。解决办法是把系统缩放调到 100% 再取色或者直接从设计源文件里读色值别从截图里取。5.2 截图取色时 JPEG 压缩的影响从图片里取色是最常见的操作但 JPEG 是有损压缩格式它会把相邻像素的颜色做混合尤其是颜色边界附近。如果你取的位置正好在色块的边缘或者图片压缩质量比较低取出来的值和原始值可能差十几个单位。同样的图存成 PNG 就没这个问题PNG 是无损的。实操上的建议是取色时尽量选色块正中间的位置离边界至少隔开十几个像素如果拿到的图只有 JPEG可以在图上放大到 800% 以上再取多看几个像素的值取出现频率最高的那个如果同一张图里同一个色块取了三次都不一样说明压缩噪点比较严重这时候要取平均值再四舍五入。另外一个细节是色彩配置文件。有些图片内嵌了 ICC 配置文件某些软件在显示时会做色彩管理转换屏幕上看到的颜色和文件里的原始数值已经不一样了。如果对色值准确性要求高取色时先把软件的色彩管理关掉或者用能直接读原始数值的工具。5.3 打印色与屏幕色的实际差距屏幕是发光体纸是反射体这两者的呈色原理根本不同所以屏幕上的#E2725B印在纸上不可能完全一致。印刷用的是 CMYK 四色油墨很多鲜艳的橙红、荧光绿、纯净蓝在 CMYK 色域里根本出不来只能逼近。这是物理限制不是设备不好。如果项目最终要落地印刷正确流程是准备一本实体色卡从色卡上选色然后根据色卡上的编号反查对应的十六进制值作为屏幕预览用。反过来先定屏幕色再去找印刷色往往会失望。我一般会在交付说明里同时给出两套值一套屏幕用的十六进制一套印刷用的色卡编号标注清楚两者是近似对应关系不承诺完全一致。中性灰和黑色在印刷上也有讲究。屏幕上的纯黑#000000印出来是四色叠印容易出现套印不准和蹭脏专业排版里大面积黑通常用单色黑加一定比例的青或者直接指定一个深灰。这些小知识平时用不上真到要出画册的时候能省不少事。6. 把对照表变成自己的工具流6.1 一个够用的转换脚本与其每次开网页查表不如手边放一个脚本。下面这个是我用了好几年的版本覆盖了日常最需要的几种转换输入输出都做了基本校验。def normalize(h): 把各种写法的颜色值统一成六位大写十六进制 h h.strip().lstrip(#).lstrip(0x).lstrip(0X) # 处理八位值按 CSS 规则取后六位视情况而定 if len(h) 8: h h[:6] # 如果是 AARRGGBB这里应改成 h[2:] if len(h) 3: h .join(c * 2 for c in h) if len(h) ! 6: raise ValueError(颜色长度不对: h) return h.upper() def hex_to_rgb(h): h normalize(h) return tuple(int(h[i:i2], 16) for i in (0, 2, 4)) def rgb_to_hex(r, g, b): for v in (r, g, b): if not 0 v 255: raise ValueError(通道值超出范围: str(v)) return #{:02X}{:02X}{:02X}.format(r, g, b) def to_bgr_int(h): 转成 Windows COLORREF 风格的十进制整数 r, g, b hex_to_rgb(h) return (b 16) | (g 8) | r print(hex_to_rgb(#E2725B)) # (226, 114, 91) print(rgb_to_hex(226, 114, 91)) # #E2725B print(to_bgr_int(#E2725B)) # 5995234这个脚本里有一个地方需要你自己确认八位色值到底是 CSS 的#RRGGBBAA还是 Android 的#AARRGGBB。我把它做成了可切换的注释因为不同项目用的格式不一样硬编码一种迟早会出错。这类看起来能用但用错场景就翻车的地方最好都在代码里留一句注释说明半年后回头看才不会抓瞎。6.2 对比度自检选颜色不只是好看的问题还要能看清。文字和背景的对比度如果太低阅读会很吃力这也是无障碍规范里明确要求的。对比度的计算不是简单的亮度相减需要先把每个通道做伽马校正再按0.2126R 0.7152G 0.0722B加权求和最后套对比度公式。场景最低对比度要求说明正文文字4.5:1小字号必须满足大号标题18pt 以上加粗3:1可适当放宽图形界面控件边界3:1按钮、输入框轮廓高标准要求7:1对可读性要求极高的场景def luminance(rgb): def f(c): c c / 255 return c / 12.92 if c 0.03928 else ((c 0.055) / 1.055) ** 2.4 r, g, b (f(x) for x in rgb) return 0.2126 * r 0.7152 * g 0.0722 * b def contrast(hex_a, hex_b): la, lb luminance(hex_to_rgb(hex_a)), luminance(hex_to_rgb(hex_b)) hi, lo max(la, lb), min(la, lb) return round((hi 0.05) / (lo 0.05), 2) print(contrast(#333333, #FFFFFF)) # 12.63很安全 print(contrast(#999999, #FFFFFF)) # 2.85不达标#999999放在白底上算出来只有 2.85属于不达标。这个结论会让不少人意外因为灰色文字配白底看着挺舒服。所以肉眼判断并不可靠尤其是长期盯屏幕之后眼睛的敏感度会下降还是跑一遍脚本比较稳妥。我做主题的时候会把所有文字色和背景色的组合都过一遍不达标的直接换深一档的灰省得后面被提意见。6.3 颜色命名与注释的约定对照表用久了会发现真正难的不是查色值而是半年后回来看到一堆色值不知道哪个是哪个。所以命名和注释这件事值得在一开始就定下来。我的习惯是这样几条。第一按用途命名而不是按颜色命名。变量名叫--color-text-primary、--color-border-subtle别叫--gray-33或者--dark。因为颜色会改用途不会变按颜色命名的变量一旦改了值名字就成了误导。第二色值统一大写统一带#前缀八位和六位不要混用。同一份配置里出现#fff、#FFFFFF、#ffffff三种写法看着就头大去重的时候也会出问题。第三每个色值后面跟一句来源说明。比如注明这个色值是从哪张设计稿取的、取色日期是什么时候、对应的印刷色卡编号是多少。这些信息当时觉得多余等到要复盘某次改版为什么选了这个颜色时就是救命的东西。第四别名和实际值分开维护。传统色、网红色这类名称放在一份映射表里指向实际的色值变量这样色值改的时候只改一处名称跟着走不会出现克莱因蓝这个名字底下挂着三个不同色值的情况。这套约定看起来是小事但配色文件一旦超过二十个变量有没有这套约定维护成本差的是几倍。我踩过的最大一次坑就是早期项目里所有颜色都是硬编码的数字后来要换品牌色全项目搜#3C8DBC搜出四百多处还不算被压缩器改写过的和写在图片里的最后只能一点点手动清。从那之后我接手的每个项目第一件事都是先把颜色变量抽出来。最后分享一个平时很实用的小习惯手机相册里存一张自己常用的灰阶和品牌色对照图临时要比对颜色的时候直接打开看比开电脑找文件快得多。对照表这东西的价值不在于收得多全而在于你需要的时候能不能立刻拿到手。