Reactで、選んだ色に近いCSSの色の名前を表示する方法
色の input で選んだ色に、royalblue や crimson のような CSS の色の名前のうち、いちばん近いものを表示する React の部品を作りました。候補は CSS の仕様にある148個の名前です。選んだ色と名前の色を Oklab という色空間の値に直し、仕様のサンプルコードにある ΔEOK2 で距離を測って、いちばん近いものを選びます。
検証環境は Ubuntu 24.04、Node.js 22.23.4、Chromium 153(headless、Playwright 1.63.0 から起動)です。使ったのは react / react-dom 19.3.1、@types/react 19.3.0、typescript 7.0.2、esbuild 0.28.2 です。.tsx を tsc で型チェックしてから esbuild で変換し、開発用ビルドを StrictMode 付きで動かしました。Firefox と Safari、スマートフォンの実機、本番用ビルド、サーバーでの描画(SSR)は試していません。選ばれた名前が人の目で見ていちばん近いかどうかを、人に見てもらって比べることもしていません。
候補にする名前は、CSS の仕様にある148個
CSS で red や lightseagreen のように書ける色の名前は、CSS Color Module Level 4 の named colors の節に、16進数の値と一緒に表で載っています。transparent と currentcolor は別の節で決められていて、この表には入っていません。
この表の名前と値を、そのまま TypeScript の配列にしました。仕様の原稿から取り出して書き出したもので、表と同じ148組です。
// CSS Color Module Level 4 の named colors(transparent を除く)
export const NAMED_COLORS: readonly [name: string, hex: string][] = [
['aliceblue', '#f0f8ff'], ['antiquewhite', '#faebd7'], ['aqua', '#00ffff'],
['aquamarine', '#7fffd4'], ['azure', '#f0ffff'], ['beige', '#f5f5dc'],
// …(148組。途中は省略)
['white', '#ffffff'], ['whitesmoke', '#f5f5f5'], ['yellow', '#ffff00'],
['yellowgreen', '#9acd32'],
];
検証環境の Chromium で、148個の名前を1つずつ color に入れて getComputedStyle() で読み出すと、どれも表の16進数と同じ rgb() になりました。名前は大文字と小文字を区別しないと仕様に書かれていて、RoyalBlue と書いても rgb(65, 105, 225) と読まれました。
148個のうち、MDN の named-color のページにあるとおり、gray と grey、aqua と cyan、fuchsia と magenta などは同じ色の別名です。同じ値をまとめると、値は139通りになりました。この記事のコードでは、同じ値の名前を1つにまとめて「gray / grey」のように並べて表示します。
なお、仕様は、名前から色を思い浮かべにくい、名前の付け方に一貫性がない(たとえば darkgray は gray より明るい)などの理由で、名前を使うことを勧めていません。この記事では、CSS を書くためではなく、選んだ色の呼び名の目安として表示するのに使います。
色を Oklab に直して、ΔEOK2 で距離を測る
仕様の「色の差の式」の説明によると、色の差の式は、2つの色がどのくらい違って見えるかを見積もるものです。仕様の Oklab の節には、Oklab は CIE Lab より見え方のむらが少ないので、色の差は3次元の空間での直線距離(ユークリッド距離)で求められる、と書かれています。この距離を ΔEOK と呼びます。
さらに、仕様の ΔEOK2 の節によると、ΔEOK は明るさの差にくらべて鮮やかさの差を小さく見積もります。そこで a と b(色みを表す2つの軸)の差を2倍してから距離をとる ΔEOK2 が示されていて、これで予測の精度が大きく上がったとされています。この記事では ΔEOK2 を使いました。
16進数から Oklab への変換は、仕様の色の変換のサンプルコードの手順と係数をそのまま使い、TypeScript の関数にまとめました。sRGB の値をガンマのかからない値に戻し、XYZ、LMS を経て Oklab にします。
export type Oklab = readonly [L: number, a: number, b: number];
// '#rrggbb' を 0〜1 の sRGB の3つの値にする
export function hexToRgb(hex: string): [number, number, number] {
const n = parseInt(hex.slice(1), 16);
return [(n >> 16) / 255, ((n >> 8) & 0xff) / 255, (n & 0xff) / 255];
}
// ガンマのかかった sRGB の値を、光の強さに比例する値(linear)に戻す
function toLinear(v: number): number {
return v <= 0.04045 ? v / 12.92 : ((v + 0.055) / 1.055) ** 2.4;
}
// sRGB → Oklab。係数は CSS Color 4 のサンプルコード(conversions.js)の値
export function hexToOklab(hex: string): Oklab {
const [r, g, b] = hexToRgb(hex).map(toLinear);
// linear sRGB → XYZ(D65)
const x = (506752 / 1228815) * r + (87881 / 245763) * g + (12673 / 70218) * b;
const y = (87098 / 409605) * r + (175762 / 245763) * g + (12673 / 175545) * b;
const z = (7918 / 409605) * r + (87881 / 737289) * g + (1001167 / 1053270) * b;
// XYZ → LMS → 立方根 → Oklab
const l = Math.cbrt(0.819022437996703 * x + 0.3619062600528904 * y - 0.1288737815209879 * z);
const m = Math.cbrt(0.0329836539323885 * x + 0.9292868615863434 * y + 0.0361446663506424 * z);
const s = Math.cbrt(0.0481771893596242 * x + 0.2642395317527308 * y + 0.6335478284694309 * z);
return [
0.210454268309314 * l + 0.7936177747023054 * m - 0.0040720430116193 * s,
1.9779985324311684 * l - 2.4285922420485799 * m + 0.450593709617411 * s,
0.0259040424655478 * l + 0.7827717124575296 * m - 0.8086757549230774 * s,
];
}
// ΔEOK2:Oklab の a と b の差を2倍してから、まっすぐな距離をとる
export function deltaEOK2(p: Oklab, q: Oklab): number {
return Math.hypot(p[0] - q[0], 2 * (p[1] - q[1]), 2 * (p[2] - q[2]));
}
変換が合っているかは、ブラウザ自身の変換と比べて確かめました。CSS の相対色の構文で oklab(from #4169e1 l a b) のように書くと、ブラウザが元の色を Oklab に直した値を getComputedStyle() で読み出せます。148個の色すべてで比べると、hexToOklab の結果との差は最大でも0.00005でした(yellow の a の値)。
いちばん近い名前を探す関数
nearestColorName は、139通りの色すべてとの ΔEOK2 を測り、いちばん小さいものを返します。名前の色の Oklab の値は、モジュールを読み込んだときに1回だけ計算しておきます。
import { NAMED_COLORS } from './namedColors';
import { deltaEOK2, hexToOklab, type Oklab } from './oklab';
export type ColorName = { names: string[]; hex: string; lab: Oklab };
// 同じ値の名前(gray と grey など)を1つにまとめ、Oklab の値を先に計算しておく
const PALETTE: ColorName[] = [];
for (const [name, hex] of NAMED_COLORS) {
const same = PALETTE.find((c) => c.hex === hex);
if (same) same.names.push(name);
else PALETTE.push({ names: [name], hex, lab: hexToOklab(hex) });
}
export type Nearest = ColorName & { distance: number };
// hex にいちばん近い名前付きの色を返す
export function nearestColorName(hex: string): Nearest {
const lab = hexToOklab(hex);
let best = PALETTE[0];
let bestDistance = Infinity;
for (const c of PALETTE) {
const d = deltaEOK2(lab, c.lab);
if (d < bestDistance) {
best = c;
bestDistance = d;
}
}
return { ...best, distance: bestDistance };
}
全部と比べる単純な探し方ですが、候補は139通りしかありません。検証環境の Chromium で nearestColorName を1万回呼ぶと50ミリ秒で、1回あたり約5マイクロ秒でした。この速さなので、次の部品では、選んだ色が変わったときに useMemo の中で毎回探しています。
色の input と並べて表示する部品
部品には、色の input、選んだ色と名前の色の2つの色見本、結果の文を並べます。
import { useId, useMemo, useState } from 'react';
import { nearestColorName } from './nearestColor';
import { hexToOklab } from './oklab';
// 明るい色の上は濃い文字、暗い色の上は白い文字にする
const textOn = (hex: string) => (hexToOklab(hex)[0] > 0.65 ? '#1f2a30' : '#ffffff');
export function ColorNameFinder() {
const [hex, setHex] = useState('#3a7bd5');
const nearest = useMemo(() => nearestColorName(hex), [hex]);
const id = useId();
const exact = nearest.hex === hex;
return (
<div className="finder">
<label htmlFor={id}>色を選ぶ</label>
<input id={id} type="color" value={hex} onChange={(e) => setHex(e.target.value)} />
<div className="swatches" aria-hidden="true">
<span style={{ background: hex, color: textOn(hex) }}>選んだ色<br />{hex}</span>
<span style={{ background: nearest.hex, color: textOn(nearest.hex) }}>
{nearest.names[0]}<br />{nearest.hex}
</span>
</div>
<output htmlFor={id} aria-live="polite">
{exact ? 'ちょうど ' : 'いちばん近いのは '}
<strong>{nearest.names.join(' / ')}</strong>
{exact ? ' です' : ` です(差 ${nearest.distance.toFixed(3)})`}
</output>
</div>
);
}
色見本は見た目のためのものなので aria-hidden で読み上げから外し、名前は <output> の文で伝えます。色見本の文字の色は、Oklab の明るさ(L)が0.65を超える色なら濃い色、それ以下なら白にしました。
名前とぴったり同じ色かどうかは、nearest.hex === hex で文字列として比べています。名前の表の16進数はすべて小文字です。検証環境の Chromium では、色の input に Playwright で #CC3366 と大文字で入れても、value は小文字の #cc3366 で読み出されたので、この比べ方で一致を判定できました。

検証環境の Chromium で、Playwright から色の input に値を入れたときの表示は次のとおりです。
| 選んだ色 | 表示された文 |
|---|---|
#3a7bd5(最初の値) |
いちばん近いのは royalblue です(差 0.092) |
#cc3366 |
いちばん近いのは crimson です(差 0.119) |
#cc9966 |
いちばん近いのは peru です(差 0.077) |
#e0e0e0 |
いちばん近いのは gainsboro です(差 0.012) |
#808080 |
ちょうど gray / grey です |
#ff0000 |
ちょうど red です |
「差」の数字は ΔEOK2 の値で、0に近いほど名前の色に近いことを表します。同じ値の名前を1つにまとめたので、#808080 では gray と grey の両方が出ました。
RGB の値のまま距離を測ると、選ばれる名前が変わる
Oklab に直さず、RGB の0〜255の値のまま距離を測っても、いちばん近い名前は選べます。そこで、R・G・B をそれぞれ16段階(0、17、34、…、255)に変えた4096色について、2つの方法で選ばれる色を比べました。違う色が選ばれたのは1,517色で、全体の37.0%でした。
a と b を2倍しない ΔEOK とも比べると、ΔEOK2 と違う色が選ばれたのは1,035色(25.3%)でした。

たとえば #cc3366 は、ΔEOK2 では crimson ですが、RGB の距離では indianred になりました。#cc9966 は、ΔEOK2 では peru、RGB の距離では darkkhaki です。暗い緑の #003300 は、ΔEOK2 と RGB の距離ではどちらも darkgreen ですが、ΔEOK では灰色がかった darkslategray になりました。
どの名前を「近い」とするかは、距離の測り方しだいで変わります。仕様には、ΔEOK2 のほかに、明るさの軸に手を加えた ΔEOKr2 や、式の込み入った ΔE2000 も載っています。また、仕様は、ΔEOKr2 のほうが ΔE2000 とよく合うとして、処理の速さが大事な実装には ΔEOKr2 を勧めています。この記事では、式がより簡単な ΔEOK2 を使いました。





