Back to portfolio
Color theory · Reference

OKLCH

Color space ที่ค่าทุกตัวตรงกับสิ่งที่ตามนุษย์มองเห็นจริง — คู่มือสำหรับ designer ที่อยากเลิกเดาสีจาก HSL แล้วหันมา "คำนวณ" สีแทน

oklch(L C H)CSS Color 4มิถุนายน 2026
หลักฐานชิ้นแรก: hue sweep เดียวกัน คนละ color spaceแถบบนคือ HSL ที่ S 100% / L 50% ทุก hue · แถบล่างคือ OKLCH ที่ L 0.72 / C 0.17 ทุก hue
hsl(h 100% 50%)
oklch(0.72 0.17 h)

กดปุ่ม grayscale แล้วดู — แถบ HSL จะกลายเป็นคลื่นสว่าง-มืดสลับกัน (เหลืองจ้า น้ำเงินทึบ) ทั้งที่ค่า L "เท่ากัน" ทุกจุด ส่วนแถบ OKLCH จะกลายเป็นสีเทาเกือบเรียบเส้นเดียว เพราะ L ของมันคือความสว่างที่ตาเห็นจริง

01

ทำไม HSL ถึงหลอกตา

ปัญหาเดียวของ 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 เท่าๆ จะดูกระโดดเป็นช่วงๆ ไม่เนียน

Triadic จาก HSL — สามสีที่ "ควรจะ" น้ำหนักเท่ากัน
hsl(60), hsl(180), hsl(300) ที่ S 100% L 50% เท่ากันทุกตัว
hsl(60)
hsl(180)
hsl(300)
เทียบกับ triadic จาก OKLCH ที่ L 0.72 / C 0.15 เท่ากัน:
oklch(…100)
oklch(…220)
oklch(…340)

นี่คือเหตุผลที่ทฤษฎีสี (analogous, complementary, triadic ฯลฯ) ใช้กับ HSL แล้ว "ไม่เวิร์กเหมือนในตำรา" — geometry ของทฤษฎีถูกต้อง แต่ wheel ที่เอามาหมุนมันบิดเบี้ยว

02

โครงสร้าง oklch(L C H)

OKLCH คือรูปทรงกระบอก (cylindrical form) ของ OKLab — color space ที่ Björn Ottosson ออกแบบในปี 2020 ให้ "perceptually uniform" คือระยะทางเท่ากันใน space = ความต่างที่ตาเห็นเท่ากัน แล้วถูกบรรจุเข้า CSS Color Level 4

oklch(L C H) อ่านว่า: สีนี้สว่างแค่ไหน (L) · อิ่ม/จัดแค่ไหน (C) · เป็นสีอะไร (H) — สามคำถามที่ designer ถามเวลาเลือกสีอยู่แล้ว แค่ตอนนี้คำตอบเป็นตัวเลขที่เชื่อถือได้
แกนช่วงค่าความหมาย
L — Lightness0 – 1 (หรือ 0–100%)ความสว่างที่ตารับรู้จริง · 0 = ดำสนิท, 1 = ขาว · ตัวเลขนี้แม่นพอจะใช้ประเมิน contrast คร่าวๆ ได้
C — Chroma0 – ~0.37 (ปลายเปิด)ความอิ่มของสี · 0 = เทาล้วน · ไม่ใช่ % เพราะเพดานจริงขึ้นกับ hue และจอ — ใน sRGB สุดได้ราว 0.37 (เฉพาะบาง hue)
H — Hue0 – 360°มุมสีบนวงล้อ · เว้นระยะแบบ perceptual — มุมห่างเท่ากัน ตาเห็นต่างเท่ากัน · ตัวเลขไม่ตรงกับ hue ของ HSL (ดู section 04)

เทียบกับเพื่อนร่วมตระกูล: lch() เป็นแนวคิดเดียวกันแต่ base บน CIELAB ซึ่งมีบั๊กเรื่อง hue shift ในโซนน้ำเงิน (ไล่ blue เข้มไปอ่อนแล้วเพี้ยนเป็นม่วง) — OKLab แก้ตรงนี้ จึงเป็นตัวที่วงการ frontend เลือกใช้เป็นมาตรฐาน เช่น Tailwind CSS v4 ที่ย้าย default palette ทั้งชุดมาเป็น OKLCH

03

สามแกน แยกทีละตัว

วิธีเข้าใจ OKLCH ที่เร็วที่สุดคือ fix สองแกน แล้วดูแกนเดียวที่วิ่ง:

Lightness — oklch(0→1 0.12 285)
L: 0.10 → 0.99
สังเกตว่าการไล่อ่อน-เข้ม "เนียนสม่ำเสมอ" ตลอดเส้น ไม่มีช่วงกระโดด — นี่คือคุณสมบัติที่ทำให้ step L เป็นช่วงเท่าๆ แล้วได้ tonal scale ที่ดีทันที
Chroma — oklch(0.70 0→0.30 285)
C: 0 → 0.30
จากเทาล้วนไปสีจัด ที่ความสว่างเท่าเดิมตลอด — ลด C คือวิธี "เบาเสียง" ของสีโดยไม่เปลี่ยนความสว่าง เหมาะกับการทำ muted/disabled state · ปลายขวาสุดเริ่มหลุด sRGB แล้ว browser จะ clamp ให้เอง
Hue — oklch(0.70 0.14 0→360)
H: 0° → 360°
หมุนรอบวงล้อโดย L,C คงที่ — ทุกสีบนเส้นนี้สว่างและอิ่มเท่ากันในสายตา นี่คือ "วงล้อสี" เวอร์ชันที่ทฤษฎีสีตั้งใจให้เป็นมาตั้งแต่แรก
04

Hue ใหม่ ไม่ตรงกับ HSL

กับดักแรกของคนย้ายจาก HSL: มุม hue ใช้แทนกันไม่ได้ เพราะ OKLCH เว้นระยะ hue ตามการรับรู้ ไม่ใช่ตามคณิตศาสตร์ RGB เช่น น้ำเงินที่เคยอยู่ 240° ใน HSL ย้ายไปอยู่ราว 264° ใน OKLCH ตารางนี้คือค่าที่ควรจำ:

สีOKLCH hueHSL 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

05

Gamut — ขอบเขตที่จอแสดงได้

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)

กฎหัวแม่มือสำหรับ design system: ถ้าต้องการ palette หลาย hue ที่ L,C เท่ากันหมดและรอด sRGB ทุกตัว — เพดานปลอดภัยอยู่ราว C ≈ 0.11–0.13 ที่ L 0.6–0.75 อยากจัดกว่านั้นต้องยอมให้บาง hue ลด C ลง หรือขยับไปใช้ Display-P3

เมื่อสีหลุด gamut, browser จะ gamut-map ให้อัตโนมัติ (CSS spec กำหนดให้ลด chroma ลงจนกว่าจะเข้า gamut โดยรักษา L กับ H ไว้) — ผลคือสีไม่พัง แต่จะ "จืดกว่าที่สั่ง" เงียบๆ ซึ่งใน design system ควรรู้ตัวก่อน ไม่ใช่ปล่อยให้ browser ตัดสินใจแทน ลองเช็คได้ใน Playground ท้ายเอกสาร

ฝั่งเครื่องมือ: ทั้ง Figma (ตั้งแต่ปี 2023) และ browser DevTools รองรับ P3/OKLCH แล้ว ส่วน @media (color-gamut: p3) ใช้เสิร์ฟสีจัดให้เฉพาะจอที่แสดงได้

06

เขียนใน CSS

รองรับครบทุก 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 ไม่พัง

07

สูตรใช้งานจริง

Tonal scale 50–900 — fix H,C แล้ว step L

หัวใจของ design tokens ยุคใหม่: lightness เป็นช่วงเท่าๆ จาก ~0.97 ลงไป ~0.27 ได้ scale ที่ contrast คาดเดาได้ทุกขั้น (ระบบจริงมัก "โค้ง" chroma ให้สูงสุดช่วงกลาง scale และลดลงที่ปลายอ่อน/เข้ม — ด้านล่างคือเวอร์ชันนั้น)
50100200300400500600700800900

UI states — ขยับ L ทีละนิด ไม่แตะ C,H

hover = L−0.07, active = L−0.12 จากสีเดิม ความ "เข้มขึ้น" จะรู้สึกเท่ากันไม่ว่า base จะเป็น hue ไหน — สิ่งที่ darken() ของ Sass/HSL ทำไม่ได้

Text บนพื้นสี — คุมด้วย ΔL

contrast ratio ของ WCAG สัมพันธ์กับ L ของ OKLCH ค่อนข้างตรง: ΔL ≥ ~0.40 มักผ่าน AA สำหรับ body text (ยังต้องเช็คจริงก่อน ship แต่ใช้เล็ง first-pass ได้แม่นมาก) เคล็ดลับสำคัญ: ให้ text ใช้ hue เดียวกับพื้น จะกลมกลืนกว่าดำ/เทาล้วน
พื้น L 0.93 + ตัวอักษร L 0.35 (hue เดียวกัน) — ΔL 0.58 ✓
พื้น L 0.55 + ตัวอักษร L 0.97 — ΔL 0.42 ✓

Dark mode — กลับ L, ลด C เล็กน้อย, คง H

สีอิ่มจัดบนพื้นมืดจะ "เรือง" แสบตา สูตรที่ใช้กันคือ mirror ค่า L (เช่น 0.93 ↔ 0.25) แล้วหั่น C ลงราว 20–30% โดย hue ไม่ขยับ — identity ของแบรนด์อยู่ครบ
Light: bg 0.95/0.03 · text 0.40/0.16
Dark: bg 0.25/0.04 · text 0.82/0.10

Color schemes จากทฤษฎีสี — ใช้ H offset ตรงๆ ได้เลย

เพราะ hue ใน OKLCH เว้นระยะแบบ perceptual ทุกสูตรเชิงมุมจึงใช้ได้ตามตำรา: analogous = H±30 · complementary = H+180 · split-comp = H+150, H+210 · triadic = H±120 — lock L,C ไว้แล้วทุกสีในชุดจะดังเท่ากัน จากนั้นค่อยสร้าง hierarchy ด้วยการลด C ของ accent
base 285
−30
+30
+180
+180, C÷2
08

Playground

ลองจูนสามแกนดูเอง — มี gamut check บอกว่าค่าที่ตั้งหลุด sRGB หรือยัง (ถ้าหลุด browser จะ clamp สีที่เห็นให้เข้า gamut อัตโนมัติ)