Ruang Warna OKLCH Dijelaskan — Mengapa Tailwind v4 Mengadopsinya
Buka sumber dari sistem desain era 2025 mana pun — shadcn/ui, Radix Themes, palet basis Tailwind v4 — dan hal pertama yang langsung menonjol adalah warnanya. Bukan kode hex, bukan triplet hsl(), melainkan sebuah fungsi yang tidak dibicarakan siapa pun tiga tahun lalu: oklch(). Tailwind v4 mengirim seluruh palet defaultnya sebagai literal OKLCH. shadcn kini menghasilkan tema yang memancarkan properti kustom OKLCH. Sistem desain Vercel dibangun ulang di sekitarnya pada 2024.
Ini bukan sekadar ikut-ikutan tren. Ada alasan matematis spesifik mengapa setiap sistem desain yang serius diam-diam berganti model warna, dan begitu Anda melihatnya, Anda tidak bisa lagi tidak melihat mengapa HSL selalu salah untuk apa yang kita gunakan.
Tulisan ini menelusuri alasan itu dari prinsip pertama, diakhiri dengan konversi yang dikerjakan langkah demi langkah dari kode hex ke OKLCH, dan memberi Anda resep migrasi untuk palet Anda sendiri.
Ketika ruang warna patah
Sistem desain memiliki tugas tonal. Sebuah tombol melayang sedikit lebih terang daripada keadaan istirahatnya. Sebuah kartu yang diredam berada satu tingkat lebih gelap daripada permukaan di sekitarnya. Sebuah cincin fokus harus terlihat lebih cerah daripada chrome netral di belakangnya. Melakukan ini dengan baik, dalam skala besar, mengharuskan “lebih terang” dan “lebih gelap” berarti hal yang sama di setiap rona di palet Anda.
Persyaratan itu mudah diabaikan ketika palet memiliki delapan warna dan tiga keadaan. Itu menjadi tidak nyaman ketika tim mulai mengirim ramp 11-langkah (50–950 dalam konvensi Tailwind), delapan warna semantik, varian terang dan gelap, dan aksen brand yang harus hidup berdampingan dengan warna sistem dari iOS, Android, dan Web. Tiba-tiba pertanyaan “apakah teal-500 ini memiliki kecerahan yang sama dengan blue-500 kami” menjadi masalah rekayasa nyata, bukan kemewahan arah seni.
HSL — model andalan sejak CSS 3 — tidak dapat menjawabnya. Dua warna HSL dengan nilai L identik dapat terlihat sangat berbeda dalam kecerahan yang dipersepsikan. Kuning HSL murni pada lightness: 50% tampak jauh lebih cerah daripada biru HSL murni pada kecerahan yang sama. Mata Anda tidak mempersepsikan kuning dan biru secara setara; HSL dirancang untuk intuitif bagi pemilih, bukan konsisten secara perseptual untuk ramp. Pada 2023, setiap sistem desain yang berskala melebihi segelintir warna menambal masalah ini dengan skrip pencampuran kustom atau override yang disetel tangan.
Yang kita butuhkan adalah model warna di mana L benar-benar berarti “kecerahan yang dipersepsikan yang akan dilaporkan manusia,” dan di mana memutar rona atau mengurangi saturasi tidak secara tak terlihat mengubah kecerahan sebagai efek samping. Model itu sudah ada di ilmu warna akademis — hanya saja ia belum menjangkau CSS.
Masalah HSL, secara konkret
Jatuhkan ini ke peramban dan lihat berdampingan:
.a { background: hsl(60 100% 50%); } /* yellow */
.b { background: hsl(240 100% 50%); } /* blue */
.c { background: hsl(120 100% 50%); } /* green */
Ketiganya memiliki L: 50%. Tidak satu pun dari mereka tampak memiliki kecerahan yang sama. Kuningnya hampir membakar; birunya terbaca hampir hitam terhadap halaman putih; hijaunya berada di antara keduanya. Jika Anda membangun keadaan hover dengan menambahkan 10% ke L, hover si kuning hampir tidak terlihat sementara hover si biru adalah pergeseran yang dramatis. Polesan interaksi Anda akhirnya bergantung pada rona mana yang kebetulan dimulai desainer.
Ini bukan bug di HSL. HSL dirancang pada 1978 untuk pemilih warna gaya paint-by-numbers, di mana pengguna akan memanipulasi rona, saturasi, dan “lightness” — didefinisikan sebagai (max(R,G,B) + min(R,G,B)) / 2 — untuk menyetel sebuah warna. Matematikanya tidak memiliki konsep persepsi manusia. Lightness di HSL adalah titik tengah geometris dari kanal sRGB, tidak lebih.
CIE — badan standar internasional untuk kolorimetri — telah mengetahui masalah ini sejak 1970-an. Mereka menerbitkan dua ruang dengan persepsi seragam, CIELAB dan CIELUV, yang mendefinisikan lightness sebagai sesuatu yang lebih dekat dengan apa yang sebenarnya dilakukan penglihatan manusia. Pada 1990-an, CIE LAB adalah standar di cetak, fotografi, dan manajemen warna. Tetapi konversinya ke RGB rumit, dan CSS tidak pernah mengadopsinya secara luas. Developer web terus menggunakan HSL bukan karena ia benar tetapi karena ia ada di sana.
CIE LAB / LCH: perbaikan akademis, dengan masalahnya sendiri
CIELAB mengambil nilai tristimulus XYZ (model bagaimana kerucut manusia merespons cahaya) dan menjalankannya melalui akar-kubik dan rotasi 2D untuk menghasilkan tiga kanal: L* (lightness, 0–100), a* (hijau ↔ merah), dan b* (biru ↔ kuning). LCH adalah ruang yang sama yang diekspresikan dalam bentuk polar: L*, C* (chroma, jarak dari netral), H* (sudut rona).
Ruang-ruang ini seragam secara perseptual dalam pengertian yang dapat diukur. Sebuah ΔE sebesar 1 — langkah satuan ke arah mana pun di ruang LAB — kira-kira adalah perbedaan warna terkecil yang dapat dideteksi pengamat terlatih. Alur kerja cetak dan prapres telah berjalan di LAB dan LCH selama puluhan tahun.
Jadi mengapa CSS tidak begitu saja mengadopsi LCH dan beralih?
Dua alasan. Pertama, CIE LAB dikalibrasi terhadap kondisi pengamatan tertentu (pengamat standar 2° di bawah iluminasi D50) yang dioptimalkan untuk reflektansi permukaan, bukan tampilan emisif. Di layar, persepsi seragamnya melayang — warna yang “sama-sama cerah” di LAB tidak selalu terlihat sama cerah di ponsel. Kedua, gamut LCH canggung. Ada warna terlihat yang dideskripsikan LAB dengan baik tetapi yang berada di luar gamut tampilan umum, dan pemetaan dari LCH ke sRGB sesekali menghasilkan pergeseran rona (biru Anda sedikit menjadi ungu ketika Anda mengurangi chroma-nya). Untuk pekerjaan sistem desain, keduanya adalah deal-breaker.
CSS Color 4 memang menambahkan lab() dan lch() pada 2021, dan keduanya berfungsi di peramban modern. Tetapi untuk masalah spesifik membangun ramp tonal yang konsisten di layar emisif, komunitas terus mencari.
OKLAB / OKLCH: wawasan Ottosson 2020
Pada Desember 2020, Björn Ottosson — seorang insinyur warna Swedia — menerbitkan makalah berjudul “A perceptual color space for image processing.” Makalahnya kecil: tiga matriks pendek, langkah akar-kubik, tanpa tabel kalibrasi, tanpa data referensi berhak cipta. Ottosson mengambil model warna IPT dan CAM16-UCS yang ada — ruang akademis dengan sifat baik tetapi matematika buruk — dan menurunkan ruang yang lebih sederhana yang mendekati perilaku perseptualnya menggunakan perkalian matriks biasa pada nilai tristimulus XYZ cahaya-linear.
Ia menyebutnya OKLAB. Bentuk polarnya adalah OKLCH.
Yang membuat OKLCH istimewa bukan kebaruan — ia cocok untuk tujuannya. Tiga sifat sekaligus:
- Lightness di OKLCH benar-benar perseptual. Kuning murni pada
L: 0.7dan biru murni padaL: 0.7tampak sama cerah pada tampilan yang dikalibrasi. Keadaan hover yang didefinisikan sebagaiL + 0.05menghasilkan pergeseran yang setara secara visual di seluruh palet. - Hue dipertahankan di bawah perubahan chroma. Jika Anda mengurangi nilai C dari
oklch(0.7 0.2 30)menjadioklch(0.7 0.1 30), rona tetap di tempatnya. Di LCH, operasi yang sama sering memperkenalkan pergeseran rona yang terlihat. Di OKLCH, Anda dapat memipihkan chroma untuk membangun varian yang diredam dari warna brand tanpa secara tidak sengaja menyimpang ke rona yang berbeda. - Matematikanya murah. Dua perkalian matriks dan satu akar-kubik. Dapat diimplementasikan dalam fungsi JavaScript 30-baris. Tidak ada tabel pencarian, tidak ada kalibrasi per-perangkat, tidak ada masalah lisensi.
Kombinasi inilah yang membuat OKLCH dapat digunakan di CSS nyata. W3C menambahkan oklch() ke CSS Color 4 pada 2022. Chrome 111 mengirimnya pada 2023. Setiap peramban evergreen mendukungnya pada pertengahan 2024. Tailwind v4 menjadikannya format palet default di tahun yang sama.
Matematika: konversi HEX → OKLCH yang dikerjakan
Mari kita telusuri #3b82f6 — blue-500 Tailwind — ke OKLCH. Ini adalah matematika yang sama yang dijalankan oleh Color Converter dan spoke HEX ke OKLCH pada setiap penekanan tombol. Mengetahui apa yang terjadi di balik layar akan membuat gotcha di bagian berikutnya lebih masuk akal.
Langkah 1: Hex ke sRGB. Pecah hex enam digit menjadi tiga pasang dan bagi masing-masing dengan 255.
const r = 0x3b / 255; // 0.231
const g = 0x82 / 255; // 0.510
const b = 0xf6 / 255; // 0.965
Ini adalah nilai sRGB yang dikodekan-gamma: nilai kanal seperti yang disimpan oleh file gambar Anda, dengan kurva non-linear yang dipanggang untuk mengompensasi bagaimana monitor memancarkan cahaya.
Langkah 2: sRGB ke linear sRGB. Hapus kurva gamma sehingga kita memiliki nilai kanal cahaya-linear. Transformasi piecewise standar dari CSS Color 4 §11.2:
const linear = (v) => v <= 0.04045 ? v / 12.92 : Math.pow((v + 0.055) / 1.055, 2.4);
const [lr, lg, lb] = [r, g, b].map(linear);
// lr ≈ 0.045, lg ≈ 0.220, lb ≈ 0.923
Langkah 3: Linear sRGB ke XYZ D65. Perkalian matriks standar, didefinisikan di CSS Color 4 §15.1:
const x = 0.4124564 * lr + 0.3575761 * lg + 0.1804375 * lb;
const y = 0.2126729 * lr + 0.7151522 * lg + 0.0721750 * lb;
const z = 0.0193339 * lr + 0.1191920 * lg + 0.9503041 * lb;
// x ≈ 0.265, y ≈ 0.231, z ≈ 0.927
Ini adalah representasi tristimulus XYZ kanonik — bentuk “panjang gelombang apa warna ini, dalam kerangka respons kerucut manusia.”
Langkah 4: XYZ ke LMS. Matriks pertama Ottosson memetakan XYZ ke ruang cone-fundamentals long/medium/short yang disetel untuk OKLAB:
const lms = [
0.8189330101 * x + 0.3618667424 * y - 0.1288597137 * z,
0.0329845436 * x + 0.9293118715 * y + 0.0361456387 * z,
0.0482003018 * x + 0.2643662691 * y + 0.6338517070 * z,
];
Langkah 5: Akar-kubikkan nilai LMS. Ini adalah langkah kompresi-perseptual — analog dengan akar-kubik di CIE LAB:
const lms_ = lms.map(Math.cbrt);
Langkah 6: LMS’ ke OKLAB. Matriks kedua Ottosson:
const L = 0.2104542553 * lms_[0] + 0.7936177850 * lms_[1] - 0.0040720468 * lms_[2];
const a = 1.9779984951 * lms_[0] - 2.4285922050 * lms_[1] + 0.4505937099 * lms_[2];
const b_ = 0.0259040371 * lms_[0] + 0.7827717662 * lms_[1] - 0.8086757660 * lms_[2];
// L ≈ 0.629, a ≈ -0.022, b_ ≈ -0.191
Itu OKLAB. Kanal lightness L ≈ 0.629 adalah apa yang dipersepsikan mata sebagai “60% kecerahan” untuk biru khusus ini.
Langkah 7: OKLAB ke OKLCH (Cartesian ke polar).
const C = Math.sqrt(a * a + b_ * b_); // 0.193
const H = (Math.atan2(b_, a) * 180 / Math.PI + 360) % 360; // 263.4
// → oklch(0.629 0.193 263.4)
Jadi #3b82f6 adalah oklch(0.629 0.193 263.4). Kecerahan perseptual 0,629, chroma 0,193, rona 263,4°.
Jika Anda membangun ramp 50–950 dari warna ini dengan memvariasikan hanya L (0,95 turun hingga 0,15 dalam 11 langkah), menahan C dan H tetap, Anda mendapatkan palet di mana setiap nuansa tampak memiliki rona yang sama secara terlihat, memudar dalam kecerahan secara seragam. Lakukan hal yang sama di HSL dan nuansa gelap bergeser ke ungu sementara nuansa terang menjadi abu-abu. Itulah kemenangannya.
Display P3 + Rec2020: mengapa OKLCH membuka gamut lebar
OKLCH tidak terbatas. Tidak seperti HSL (terbatas pada sRGB) dan bahkan LCH (dikalibrasi untuk permukaan reflektif), OKLCH tidak memiliki gamut implisit. Anda dapat menulis oklch(0.7 0.25 30) dan menghasilkan merah cerah yang berada di dalam volume warna Display P3 tetapi di luar sRGB. Pada iPhone atau MacBook terbaru, ia di-render. Pada monitor lama, peramban secara otomatis snap ke representasi sRGB terdekat.
Ini penting karena Apple, Samsung, dan W3C semuanya menghabiskan akhir 2010-an mengirim perangkat keras gamut-lebar. MacBook Pro 14” / 16” dengan mini-LED dikirim dengan P3 secara default. iPhone 15 Pro me-render Display P3 di Safari. Flagship Android mengirim panel Rec2020. Pada 2025, fraksi substansial dari lalu lintas sistem desain berada di perangkat keras gamut-lebar yang dapat menampilkan warna yang HSL/sRGB sama sekali tidak dapat diekspresikan.
OKLCH memungkinkan Anda menulis warna-warna itu tanpa memasang deklarasi @media (color-gamut: p3) terpisah. Peramban menangani fallback. Sistem desain Anda mendapatkan “gunakan merah paling cerah yang dapat di-render perangkat” siap pakai.
Inilah juga mengapa OKLCH adalah format yang tepat untuk token desain. Variabel --brand di OKLCH adalah deskripsi intent yang independen-perangkat. Peramban mencari tahu apa yang harus di-render pada tampilan apa pun yang dimiliki pengguna, dan kode Anda portabel di seluruh CSS, SwiftUI (yang secara native mendukung Display P3 Color), Android Compose (sadar-Rec2020), dan Flutter.
Tailwind v4 dan revolusi token-desain
Tailwind v4 — dirilis pada 2024 — adalah titik infleksi yang mengubah OKLCH dari penelitian menjadi default industri. Penulis Tailwind membuat tiga panggilan opinionated:
- Palet default adalah OKLCH. Slate, gray, zinc, neutral, stone — setiap warna Tailwind didefinisikan dalam
oklch()di sumber. Ramp 50–950 seragam dalam kecerahan perseptual secara konstruksi. - Tema kustom menggunakan blok
@themedengan literal OKLCH. Warna brand didefinisikan sebagai tokenoklch(); utilitas di hilir (bg-brand-500,text-brand-300) dihasilkan. - Tidak diperlukan upacara fallback. Peramban tanpa dukungan OKLCH berada di bawah baseline yang didokumentasikan Tailwind v4.
Keputusan terakhir itulah yang menjadikan adopsi pilihan bebas-footgun. Baru pada 2023, desainer harus mengirim versi oklch() dan hsl() dari setiap warna agar versi Safari yang lebih lama tidak rusak. Dengan Tailwind v4, baseline adalah peramban dari 2023 atau lebih baru, dan OKLCH bekerja di mana-mana.
Generator tema shadcn/ui mengikuti pola yang sama: masukkan warna brand Anda, dapatkan ramp OKLCH. Sistem desain Vercel menggunakan OKLCH untuk warna semantiknya. Skala warna Radix Themes didefinisikan di OKLCH. Komunitas telah konvergen.
Migrasi praktis: palet HEX → palet OKLCH
Jika Anda memiliki palet berbasis-hex hari ini, migrasinya bersifat mekanis. Berikut resepnya:
1. Putuskan struktur ramp Anda. 50–950 Tailwind (11 stop) adalah default de-facto dan layak diikuti kecuali Anda memiliki alasan spesifik sebaliknya. Stop pada L = 0,97, 0,93, 0,86, 0,76, 0,63, 0,50, 0,42, 0,34, 0,26, 0,18, 0,10 memberikan ramp perseptual yang mulus.
2. Konversi hex brand Anda ke OKLCH. Gunakan alat Color Converter atau HEX ke OKLCH. Anda akan mendapatkan triplet seperti oklch(0.629 0.193 263.4). Catat nilai H — ini adalah rona brand Anda.
3. Tahan C dan H konstan; variasikan L. Bangun ramp dengan memancarkan:
--brand-50: oklch(0.97 0.193 263.4);
--brand-100: oklch(0.93 0.193 263.4);
...
--brand-950: oklch(0.10 0.193 263.4);
4. Setel stop ekstrem. Pada L sangat rendah (≤ 0,20) dan L sangat tinggi (≥ 0,95), nilai chroma tinggi jatuh di luar sRGB. Kurangi C untuk stop tersebut, atau terima auto-snap peramban. Default Tailwind mengurangi chroma ke arah kedua ujung — salin pola itu.
5. Definisikan alias semantik. --surface: var(--brand-50), --surface-elevated: var(--brand-100), --text-primary: var(--brand-900), dst. Sekarang token desain Anda terbaca sebagai intent, bukan sebagai warna.
6. Verifikasi kontras. Gunakan APCA Lc atau rasio kontras WCAG 2 untuk mengonfirmasi setiap pasangan --text-* terhadap setiap pasangan --surface-* memenuhi standar aksesibilitas Anda. Karena L OKLCH bersifat perseptual, matematika kontras lebih andal daripada di HSL.
Sebuah tim yang menjalankan migrasi ini pada palet warisan 60-warna biasanya mendarat dengan palet OKLCH yang lebih kecil dan lebih seragam berisi 30–40 token dalam satu sore. Palet baru dikirim lebih kecil, menghasilkan CSS yang lebih kecil, dan menghasilkan gerak tonal yang terlihat lebih baik di keadaan hover dan keadaan disabled tanpa penyetelan ekstra apa pun.
Jebakan dan cara menanganinya
Beberapa hal yang perlu diketahui sebelum memulai:
Peringatan di luar gamut. Beberapa nilai OKLCH jatuh di luar Display P3 atau sRGB. Peramban modern menangani snap ke warna valid terdekat secara otomatis, tetapi snap-nya bersifat lossy: oklch(0.7 0.25 30) Anda mungkin di-render sedikit kurang jenuh daripada yang Anda tulis. Alat seperti baris gamut Color Converter memberi tahu apakah warna Anda aman-sRGB, aman-P3, atau aman-Rec2020, dan menawarkan snap-ke-sRGB satu-klik sehingga apa yang Anda tulis adalah apa yang Anda lihat.
Keanehan chroma sub-piksel. Chroma OKLCH tidak terbatas, tetapi rentang yang berguna adalah kira-kira 0 hingga ~0,4 untuk warna yang terlihat. Nilai di atas 0,4 hanya dapat dicapai dengan cahaya laser monokromatik — bukan warna fisik yang dapat di-render tampilan. Color Converter membatasi slider chroma pada 0,4 karena alasan ini; nilai di luar itu tidak menghasilkan perbedaan yang dapat dipersepsikan pada tampilan nyata mana pun.
Dukungan peramban, gaya-2025. Chrome 111+, Safari 15.4+, Firefox 113+ semua mendukung oklch() secara native. Peramban pra-2023 tidak. Jika Anda harus mendukung IE/Edge warisan atau Safari mobile yang lebih lama (1–3% lalu lintas tergantung audiens Anda), Anda dapat memasangkan deklarasi OKLCH dengan fallback hex menggunakan @supports (color: oklch(0 0 0)) — tetapi untuk token sistem desain yang dikirim pada 2025, biaya fallback sering kali melebihi manfaat warisan.
Permanensi kode hex. OKLCH adalah untuk intent sistem desain. CMS Anda mungkin masih membutuhkan nilai hex karena alasan warisan (tanda tangan email, dokumen Office, daftar periksa aset brand). Pertahankan tabel pencarian yang dihasilkan yang memancarkan hex ber-snap-sRGB untuk setiap token OKLCH, tetapi jangan menulis dalam hex.
Jangan rancukan OKLCH dan OKLAB. OKLAB adalah bentuk persegi panjang (kanal L, a, b); OKLCH adalah ruang warna yang sama dalam bentuk polar (L, C, H). Anda mengonversi antara keduanya dengan satu langkah cartesian↔polar. Gunakan OKLCH untuk token (lebih mudah dibaca, lebih mudah di-ramp); gunakan OKLAB secara internal jika Anda perlu menginterpolasi atau memadukan warna.
Coba pada palet Anda sendiri
Cara tercepat untuk melihat apa yang kami jelaskan di sini dalam tindakan adalah menjatuhkan hex brand ke Color Converter. Ketik warna brand Anda di bidang HEX dan baca output OKLCH. Lalu gerakkan slider di sisi OKLCH dan saksikan bagaimana rona yang sama tetap menjadi rona yang sama saat Anda mengurangi chroma, dan bagaimana kecerahan yang sama tetap menjadi kecerahan yang sama saat Anda memutar rona. Setelah beberapa menit Anda akan memiliki rasa intuitif mengapa HSL selalu menjadi alat yang salah untuk ramp tonal, dan mengapa setiap sistem desain yang serius telah beralih.
Untuk konversi HEX-ke-OKLCH spesifik, gunakan HEX ke OKLCH — matematika yang sama dengan artikel ini, dengan satu manfaat tambahan: ia menunjukkan klasifikasi gamut (sRGB / Display P3 / Rec2020) sehingga Anda tahu warna brand mana yang aman di mana-mana versus mana yang membutuhkan perangkat gamut-lebar untuk di-render sepenuhnya.
Itu dia OKLCH. Layak untuk migrasi. Dilakukan dengan baik, Anda tidak akan pernah menulis hsl() lagi.