Color space ที่ค่าทุกตัวตรงกับสิ่งที่ตามนุษย์มองเห็นจริง — คู่มือสำหรับ designer ที่อยากเลิกเดาสีจาก HSL แล้วหันมา "คำนวณ" สีแทน
กดปุ่ม grayscale แล้วดู — แถบ HSL จะกลายเป็นคลื่นสว่าง-มืดสลับกัน (เหลืองจ้า น้ำเงินทึบ) ทั้งที่ค่า L "เท่ากัน" ทุกจุด ส่วนแถบ OKLCH จะกลายเป็นสีเทาเกือบเรียบเส้นเดียว เพราะ L ของมันคือความสว่างที่ตาเห็นจริง
ปัญหาเดียวของ HSL ที่ทำให้ทุกอย่างพังคือ: ค่า L ของมันไม่ใช่ความสว่างที่ตาเห็น มันเป็นแค่ค่าเฉลี่ยทางคณิตศาสตร์ของ RGB
ตามนุษย์ไวต่อแสงสีเขียว-เหลืองมากกว่าสีน้ำเงินหลายเท่า ดังนั้น hsl(60 100% 50%) (เหลือง) กับ hsl(240 100% 50%) (น้ำเงิน) ที่ "L 50% เท่ากัน" จริงๆ แล้วเหลืองสว่างกว่าน้ำเงินราว 3 เท่าในสายตาเรา ผลที่ตามมาในงานจริง:
— Palette ที่ generate จากสูตร HSL (เช่นหมุน hue อย่างเดียว) จะได้สีที่ "ดัง" ไม่เท่ากัน ต้องมานั่ง hand-tune ทีละสี
— Contrast ratio คาดเดาไม่ได้ — สอง pair ที่ ΔL เท่ากันใน HSL อาจผ่าน/ไม่ผ่าน WCAG คนละผล
— Tonal scale (50–900) ที่ step L เท่าๆ จะดูกระโดดเป็นช่วงๆ ไม่เนียน
นี่คือเหตุผลที่ทฤษฎีสี (analogous, complementary, triadic ฯลฯ) ใช้กับ HSL แล้ว "ไม่เวิร์กเหมือนในตำรา" — geometry ของทฤษฎีถูกต้อง แต่ wheel ที่เอามาหมุนมันบิดเบี้ยว
OKLCH คือรูปทรงกระบอก (cylindrical form) ของ OKLab — color space ที่ Björn Ottosson ออกแบบในปี 2020 ให้ "perceptually uniform" คือระยะทางเท่ากันใน space = ความต่างที่ตาเห็นเท่ากัน แล้วถูกบรรจุเข้า CSS Color Level 4
| แกน | ช่วงค่า | ความหมาย |
|---|---|---|
L — Lightness | 0 – 1 (หรือ 0–100%) | ความสว่างที่ตารับรู้จริง · 0 = ดำสนิท, 1 = ขาว · ตัวเลขนี้แม่นพอจะใช้ประเมิน contrast คร่าวๆ ได้ |
C — Chroma | 0 – ~0.37 (ปลายเปิด) | ความอิ่มของสี · 0 = เทาล้วน · ไม่ใช่ % เพราะเพดานจริงขึ้นกับ hue และจอ — ใน sRGB สุดได้ราว 0.37 (เฉพาะบาง hue) |
H — Hue | 0 – 360° | มุมสีบนวงล้อ · เว้นระยะแบบ perceptual — มุมห่างเท่ากัน ตาเห็นต่างเท่ากัน · ตัวเลขไม่ตรงกับ hue ของ HSL (ดู section 04) |
เทียบกับเพื่อนร่วมตระกูล: lch() เป็นแนวคิดเดียวกันแต่ base บน CIELAB ซึ่งมีบั๊กเรื่อง hue shift ในโซนน้ำเงิน (ไล่ blue เข้มไปอ่อนแล้วเพี้ยนเป็นม่วง) — OKLab แก้ตรงนี้ จึงเป็นตัวที่วงการ frontend เลือกใช้เป็นมาตรฐาน เช่น Tailwind CSS v4 ที่ย้าย default palette ทั้งชุดมาเป็น OKLCH
วิธีเข้าใจ OKLCH ที่เร็วที่สุดคือ fix สองแกน แล้วดูแกนเดียวที่วิ่ง:
oklch(0→1 0.12 285)oklch(0.70 0→0.30 285)oklch(0.70 0.14 0→360)กับดักแรกของคนย้ายจาก HSL: มุม hue ใช้แทนกันไม่ได้ เพราะ OKLCH เว้นระยะ hue ตามการรับรู้ ไม่ใช่ตามคณิตศาสตร์ RGB เช่น น้ำเงินที่เคยอยู่ 240° ใน HSL ย้ายไปอยู่ราว 264° ใน OKLCH ตารางนี้คือค่าที่ควรจำ:
| สี | OKLCH hue | HSL hue (เดิม) | |
|---|---|---|---|
| แดง | ~25° | 0° | |
| ส้ม | ~60° | 30° | |
| เหลือง | ~100° | 60° | |
| เขียว | ~145° | 120° | |
| Cyan | ~195° | 180° | |
| ฟ้า | ~240–250° | 210° | |
| น้ำเงิน | ~264° | 240° | |
| ม่วง | ~300° | 270° | |
| Magenta | ~330° | 300° | |
| ชมพู | ~350–10° | 330° |
ข้อดีที่แลกมา: ความสัมพันธ์เชิงมุมทั้งหมดในทฤษฎีสี (ตรงข้าม 180°, triadic 120°, analogous ±30°) กลายเป็น ความสัมพันธ์ที่ตาเห็นจริง — H+180 ใน OKLCH คือสีที่ "รู้สึกตรงข้าม" จริงๆ ไม่ใช่แค่ตรงข้ามในสูตร RGB
OKLCH อธิบายสีได้กว้างกว่าที่จอทั่วไปแสดงได้ — มันครอบคลุมทั้ง sRGB, Display-P3 และไกลกว่านั้น นั่นแปลว่า ไม่ใช่ทุก combination ของ L C H จะมีอยู่จริงบนจอ
รูปทรงของ gamut ใน OKLCH ไม่ใช่กล่องสี่เหลี่ยม แต่เหมือนภูเขาที่ยอดสูงต่ำไม่เท่ากันตาม hue:
— เพดาน C ของ sRGB อยู่ราว 0.37 แต่ทำได้เฉพาะแดง-magenta บางจุดเท่านั้น
— Cyan/teal เป็นโซนที่เพดาน C ต่ำสุด (ราว 0.15 ที่ mid lightness)
— เหลืองจัดมีได้เฉพาะที่ L สูงมาก (~0.9+) · น้ำเงินจัดมีได้เฉพาะที่ L ต่ำ (~0.45)
เมื่อสีหลุด gamut, browser จะ gamut-map ให้อัตโนมัติ (CSS spec กำหนดให้ลด chroma ลงจนกว่าจะเข้า gamut โดยรักษา L กับ H ไว้) — ผลคือสีไม่พัง แต่จะ "จืดกว่าที่สั่ง" เงียบๆ ซึ่งใน design system ควรรู้ตัวก่อน ไม่ใช่ปล่อยให้ browser ตัดสินใจแทน ลองเช็คได้ใน Playground ท้ายเอกสาร
ฝั่งเครื่องมือ: ทั้ง Figma (ตั้งแต่ปี 2023) และ browser DevTools รองรับ P3/OKLCH แล้ว ส่วน @media (color-gamut: p3) ใช้เสิร์ฟสีจัดให้เฉพาะจอที่แสดงได้
รองรับครบทุก browser หลักตั้งแต่ปี 2023 (Chrome 111+, Safari 15.4+, Firefox 113+) ใช้ใน production ได้เลยไม่ต้องมี fallback สำหรับ browser ปัจจุบัน
/* รูปแบบพื้นฐาน — ช่องว่างคั่น ไม่มี comma */ color: oklch(0.55 0.21 285); color: oklch(55% 0.21 285); /* L เป็น % ก็ได้ */ color: oklch(0.55 0.21 285 / 0.6); /* alpha ต่อท้ายด้วย / */ /* ผสมสีใน space ที่ตาเชื่อถือได้ — ไม่เกิดโซนเทาขุ่นตรงกลาง */ color: color-mix(in oklch, var(--brand) 70%, white); /* Relative color syntax — แตกสีลูกจากสีแม่ตัวเดียว */ /* (รองรับครบทุก browser ตั้งแต่กลางปี 2024) */ --brand: oklch(0.55 0.21 285); --brand-hover: oklch(from var(--brand) calc(l - 0.07) c h); --brand-bg: oklch(from var(--brand) 0.96 0.03 h); --brand-border: oklch(from var(--brand) 0.85 0.06 h); --brand-text: oklch(from var(--brand) 0.35 0.12 h); /* Gradient ที่ interpolate ใน oklch — ไม่ผ่านโซนเทา */ background: linear-gradient(in oklch, oklch(0.7 0.15 25), oklch(0.7 0.15 264));
บรรทัด relative color คือ game changer ตัวจริง: ประกาศ brand color ตัวเดียว แล้ว derive ทั้ง hover, background tint, border, text ด้วยสูตร — เปลี่ยน hue ของแบรนด์ทีเดียว ทุก state ตามมาเองโดย contrast ไม่พัง
ลองจูนสามแกนดูเอง — มี gamut check บอกว่าค่าที่ตั้งหลุด sRGB หรือยัง (ถ้าหลุด browser จะ clamp สีที่เห็นให้เข้า gamut อัตโนมัติ)