Skip to content
Powrót do bloga
Poradniki

Przestrzeń barw OKLCH wyjaśniona — dlaczego Tailwind v4 ją przyjął

Dlaczego OKLCH stał się standardem systemów designu w latach 2024–2026. Czym różni się od HSL i LCH, z rozpisaną konwersją HEX→OKLCH na konkretach.

14 min czytania

Przestrzeń barw OKLCH wyjaśniona — dlaczego Tailwind v4 ją przyjął

Otwórz źródło dowolnego systemu designu z ery 2025 — shadcn/ui, Radix Themes, domyślnej palety Tailwind v4 — i pierwszym, co rzuca się w oczy, są kolory. Nie kody hex, nie trójki hsl(), lecz funkcja, o której trzy lata temu nikt nie mówił: oklch(). Tailwind v4 dostarcza całą swoją domyślną paletę jako literały OKLCH. shadcn generuje teraz motywy, które emitują niestandardowe właściwości OKLCH. System designu Vercela został przebudowany wokół niej w 2024.

To nie jest podążanie za modą. Istnieje konkretny, matematyczny powód, dla którego każdy poważny system designu po cichu zmieniał model koloru, a kiedy go zobaczysz, nie da się go już odzobaczyć — zobaczysz, dlaczego HSL od zawsze był złym narzędziem do tego, do czego go używaliśmy.

Ten wpis przeprowadzi cię przez ten powód od pierwszych zasad, zakończy rozpisaną konwersją kodu hex do OKLCH i da ci przepis na migrację własnej palety.

Kiedy przestrzenie barw się załamały

Systemy designu mają zadanie tonalne. Przycisk w stanie hover ma być nieco jaśniejszy niż w stanie spoczynkowym. Wyciszona karta siedzi o jeden stopień ciemniej niż powierzchnia wokół niej. Pierścień focus musi być widocznie jaśniejszy od neutralnego chrome za nim. Robienie tego dobrze, na skalę, wymaga, by „jaśniej” i „ciemniej” znaczyło to samo dla każdej barwy w palecie.

To wymaganie łatwo było ignorować, gdy palety miały osiem kolorów i trzy stany. Stało się niewygodne, gdy zespoły zaczęły dostarczać 11-stopniowe rampy (50–950 w konwencji Tailwinda), osiem kolorów semantycznych, warianty jasny i ciemny oraz akcenty marki, które musiały współistnieć z kolorami systemowymi z iOS, Androida i Webu. Nagle pytanie „czy ta teal-500 ma tę samą jasność co nasza blue-500” stało się realnym problemem inżynieryjnym, a nie luksusem dyrekcji artystycznej.

HSL — koń pociągowy od czasów CSS 3 — nie potrafił na to odpowiedzieć. Dwa kolory HSL o identycznych wartościach L mogą wyglądać dramatycznie różnie pod względem postrzeganej jasności. Czysty żółty HSL przy lightness: 50% wygląda znacznie jaśniej niż czysty niebieski HSL przy tej samej jasności. Twoje oko nie postrzega żółtego i niebieskiego jednakowo; HSL zaprojektowano tak, by był intuicyjny dla pickerów, a nie percepcyjnie spójny dla ramp. Do 2023 każdy system designu, który skalował się powyżej garstki kolorów, łatał to obejściem przez własne skrypty mieszające lub ręcznie dostrojone nadpisania.

Potrzebowaliśmy modelu koloru, w którym L faktycznie oznacza „postrzeganą jasność, którą zgłosiłby człowiek”, i w którym obrót barwy lub zmniejszenie nasycenia nie zmieniają niewidzialnie jasności jako efekt uboczny. Taki model istniał w akademickiej nauce o kolorze — po prostu nie dotarł jeszcze do CSS.

Problem HSL na konkretach

Wrzuć te wartości do przeglądarki i postaw je obok siebie:

.a { background: hsl(60 100% 50%); }   /* yellow */
.b { background: hsl(240 100% 50%); }  /* blue */
.c { background: hsl(120 100% 50%); }  /* green */

Wszystkie trzy mają L: 50%. Żaden z nich nie wygląda, jakby miał tę samą jasność. Żółty niemal pali; niebieski na białej stronie czyta się prawie jak czerń; zielony siedzi między nimi. Jeśli zbudujesz stan hover, dodając 10% do L, hover dla żółtego będzie ledwie widoczny, podczas gdy hover dla niebieskiego będzie dramatyczną zmianą. Twoja interakcyjna szlifka kończy zależna od tego, od której barwy projektant akurat zaczął.

To nie jest błąd w HSL. HSL zaprojektowano w 1978 dla pickerów koloru typu „pomaluj według numerów”, w których użytkownicy manipulowali barwą, nasyceniem i „jasnością” — zdefiniowaną jako (max(R,G,B) + min(R,G,B)) / 2 — by trafić w kolor. Matematyka nie ma pojęcia o ludzkiej percepcji. Jasność w HSL to geometryczny środek kanałów sRGB, nic więcej.

CIE — międzynarodowy organ normalizacyjny ds. kolorymetrii — wiedział o tym problemie od lat 70. Opublikował dwie percepcyjnie jednorodne przestrzenie, CIELAB i CIELUV, które definiowały jasność jako coś bliższego temu, co faktycznie robi ludzki wzrok. Do lat 90. CIE LAB było standardem w druku, fotografii i zarządzaniu kolorem. Ale jego konwersja do RGB jest zawiła, a CSS nigdy nie przyjął jej szeroko. Webdeveloperzy używali HSL nie dlatego, że był słuszny, lecz dlatego, że był pod ręką.

CIE LAB / LCH: akademicki fix z własnymi problemami

CIELAB bierze wartość trójchromatyczną XYZ (model tego, jak ludzkie czopki reagują na światło) i przepuszcza ją przez pierwiastek sześcienny oraz obrót 2D, by wyprodukować trzy kanały: L* (jasność, 0–100), a* (zielony ↔ czerwony) i b* (niebieski ↔ żółty). LCH to ta sama przestrzeń wyrażona w formie biegunowej: L*, C* (chroma, odległość od neutralnego), H* (kąt barwy).

Te przestrzenie są percepcyjnie jednorodne w mierzalnym sensie. ΔE równe 1 — krok jednostkowy w dowolnym kierunku w przestrzeni LAB — to z grubsza najmniejsza różnica koloru, jaką wytrenowany obserwator potrafi wykryć. Przepływy pracy druku i prepressu funkcjonują na LAB i LCH od dziesięcioleci.

Dlaczego więc CSS po prostu nie przyjął LCH i nie poszedł dalej?

Z dwóch powodów. Po pierwsze, CIE LAB skalibrowano pod konkretne warunki obserwacji (obserwator standardowy 2° w iluminancie D50) zoptymalizowane pod powierzchniową odbiciowość, a nie pod ekrany emisyjne. Na ekranach jego percepcyjna jednorodność dryfuje — kolory, które są „równie jasne” w LAB, nie zawsze wyglądają na równie jasne na telefonie. Po drugie, gamut LCH jest niewygodny. Istnieją widzialne kolory, które LAB opisuje dobrze, lecz które leżą poza powszechnymi gamutami wyświetlaczy, a mapowanie z LCH do sRGB okazjonalnie produkuje przesunięcia barwy (twój niebieski lekko fioletowieje, gdy zmniejszasz jego chroma). Dla pracy nad systemem designu oba te punkty to deal-breakery.

CSS Color 4 faktycznie dodał lab() i lch() w 2021 i działają one w nowoczesnych przeglądarkach. Ale dla konkretnego problemu budowania spójnych ramp tonalnych na ekranach emisyjnych społeczność wciąż szukała dalej.

OKLAB / OKLCH: olśnienie Ottossona z 2020

W grudniu 2020 Björn Ottosson — szwedzki inżynier koloru — opublikował artykuł zatytułowany „A perceptual color space for image processing”. Praca była mała: trzy krótkie macierze, krok pierwiastkowania sześciennego, brak tablic kalibracyjnych, brak danych referencyjnych objętych prawami autorskimi. Ottosson wziął istniejące modele kolorów IPT i CAM16-UCS — akademickie przestrzenie o dobrych właściwościach, lecz złej matematyce — i wyprowadził prostszą przestrzeń, która przybliżała ich percepcyjne zachowanie, używając zwykłego mnożenia macierzy na wartościach trójchromatycznych XYZ w świetle liniowym.

Nazwał ją OKLAB. Forma biegunowa to OKLCH.

To, co czyni OKLCH wyjątkowym, to nie nowość — lecz przystosowanie do celu. Trzy właściwości razem:

  1. Jasność w OKLCH jest naprawdę percepcyjna. Czysty żółty przy L: 0.7 i czysty niebieski przy L: 0.7 wyglądają na tę samą jasność na skalibrowanym wyświetlaczu. Stany hover zdefiniowane jako L + 0.05 produkują wizualnie równoważne przesunięcia w całej palecie.
  2. Barwa jest zachowana przy zmianach chromy. Jeśli zmniejszysz wartość C w oklch(0.7 0.2 30) do oklch(0.7 0.1 30), barwa pozostaje na miejscu. W LCH ta sama operacja często wprowadza widoczne przesunięcie barwy. W OKLCH możesz spłaszczyć chroma, by zbudować wyciszony wariant koloru marki, bez tego, by przypadkiem dryfował w stronę innej barwy.
  3. Matematyka jest tania. Dwa mnożenia macierzy i jeden pierwiastek sześcienny. Implementowalne w 30-liniowej funkcji JavaScript. Brak tablic lookupowych, brak kalibracji per-urządzenie, brak problemów licencyjnych.

Ta kombinacja sprawiła, że OKLCH stał się użyteczny w prawdziwym CSS. W3C dodał oklch() do CSS Color 4 w 2022. Chrome 111 dostarczył go w 2023. Każda aktualnie aktualizowana przeglądarka obsługiwała go do połowy 2024. Tailwind v4 uczynił go domyślnym formatem palety w tym samym roku.

Matematyka: rozpisana konwersja HEX → OKLCH

Przeprowadźmy #3b82f6blue-500 Tailwinda — do OKLCH. To ta sama matematyka, którą konwerter kolorów oraz odgałęzienie HEX na OKLCH wykonują przy każdym naciśnięciu klawisza. Wiedza o tym, co dzieje się pod maską, sprawi, że pułapki w następnej sekcji nabiorą sensu.

Krok 1: Hex do sRGB. Podziel 6-cyfrowy hex na trzy pary i podziel każdą przez 255.

const r = 0x3b / 255; // 0.231
const g = 0x82 / 255; // 0.510
const b = 0xf6 / 255; // 0.965

To wartości sRGB zakodowane gammą: wartości kanału w postaci, w jakiej przechowuje je twój plik obrazu, z wpieczoną krzywą nieliniową, która kompensuje sposób, w jaki monitory emitują światło.

Krok 2: sRGB do linear sRGB. Usuń krzywą gamma, żebyśmy mieli wartości kanału w świetle liniowym. Standardowe przekształcenie fragmentaryczne z 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

Krok 3: Linear sRGB do XYZ D65. Standardowe mnożenie macierzy, zdefiniowane w 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

To kanoniczna reprezentacja trójchromatyczna XYZ — forma odpowiadająca na „jakie długości fal ma ten kolor, w kategoriach odpowiedzi ludzkich czopków”.

Krok 4: XYZ do LMS. Pierwsza macierz Ottossona mapuje XYZ do przestrzeni fundamentów czopków long/medium/short dostrojonej pod 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,
];

Krok 5: Pierwiastek sześcienny z wartości LMS. To krok kompresji percepcyjnej — analogiczny do pierwiastka sześciennego w CIE LAB:

const lms_ = lms.map(Math.cbrt);

Krok 6: LMS’ do OKLAB. Druga macierz Ottossona:

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

To jest OKLAB. Kanał jasności L ≈ 0.629 to to, co oko postrzega jako „60% jasności” dla tego konkretnego niebieskiego.

Krok 7: OKLAB do OKLCH (kartezjańskie do biegunowych).

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)

Czyli #3b82f6 to oklch(0.629 0.193 263.4). Jasność 0.629, chroma 0.193, barwa 263,4°.

Jeśli z tego koloru zbudujesz rampę 50–950, zmieniając wyłącznie L (od 0,95 w dół do 0,15 w 11 krokach), utrzymując C i H stałe, otrzymasz paletę, w której każdy odcień jest widocznie tą samą barwą, równomiernie ściemniającą się w jasności. Zrób to samo w HSL, a ciemne odcienie przesuną się w stronę fioletu, podczas gdy jasne odcienie staną się szare. To jest wygrana.

Display P3 + Rec2020: dlaczego OKLCH odblokowuje szeroki gamut

OKLCH jest nieograniczony. W odróżnieniu od HSL (zamkniętego w sRGB) i nawet LCH (skalibrowanego pod powierzchnie odbijające), OKLCH nie ma niejawnego gamutu. Możesz zapisać oklch(0.7 0.25 30) i wyprodukować żywą czerwień, która siedzi wewnątrz objętości koloru Display P3, lecz poza sRGB. Na świeżym iPhonie albo MacBooku renderuje się. Na starszym monitorze przeglądarka przyciąga ją automatycznie do najbliższej reprezentacji sRGB.

To ma znaczenie, ponieważ Apple, Samsung i W3C spędziły późne lata 2010. dostarczając sprzęt o szerokim gamucie. MacBook Pro 14”/16” z mini-LED dostarcza P3 domyślnie. iPhone 15 Pro renderuje Display P3 w Safari. Flagowce Androida dostarczają panele Rec2020. Do 2025 znaczna część ruchu w systemach designu odbywa się na sprzęcie o szerokim gamucie, który potrafi pokazać kolory, jakich HSL/sRGB po prostu nie potrafi wyrazić.

OKLCH pozwala ci pisać te kolory bez doczepiania osobnej deklaracji @media (color-gamut: p3). Przeglądarka zajmuje się fallbackiem. Twój system designu dostaje „użyj najjaśniejszej czerwieni, jaką urządzenie potrafi wyrenderować” prosto z pudełka.

To także jest powód, dla którego OKLCH jest właściwym formatem dla tokenów designu. Zmienna --brand w OKLCH to niezależny od urządzenia opis intencji. Przeglądarka rozpracowuje, co wyrenderować na dowolnym wyświetlaczu, jaki ma użytkownik, a twój kod jest przenośny między CSS, SwiftUI (które natywnie obsługuje Display P3 Color), Android Compose (świadomy Rec2020) i Flutter.

Tailwind v4 i rewolucja tokenów designu

Tailwind v4 — wydany w 2024 — był punktem przegięcia, który przeniósł OKLCH z badań do branżowego domyślnego standardu. Autorzy Tailwinda podjęli trzy opinionated decyzje:

  1. Domyślna paleta jest w OKLCH. Slate, gray, zinc, neutral, stone — każdy kolor Tailwinda jest zdefiniowany w oklch() w źródle. Rampy 50–950 są z konstrukcji jednorodne w jasności percepcyjnej.
  2. Motywy własne używają bloków @theme z literałami OKLCH. Kolory marki definiuje się jako tokeny oklch(); narzędzia downstream (bg-brand-500, text-brand-300) są generowane.
  3. Bez ceremonii fallbacku. Przeglądarki bez wsparcia dla OKLCH są poniżej udokumentowanego baseline’u Tailwind v4.

Ta ostatnia decyzja sprawiła, że adopcja stała się wyborem wolnym od strzelania sobie w stopę. Jeszcze w 2023 projektanci musieli dostarczać zarówno wersję oklch(), jak i hsl() każdego koloru, żeby starsze wersje Safari nie posypały się. W Tailwind v4 baseline to przeglądarki z 2023 lub nowsze, a OKLCH działa wszędzie.

Generator motywów shadcn/ui podąża za tym samym wzorcem: wpisz swój kolor marki, dostań na wyjściu rampę OKLCH. System designu Vercela używa OKLCH dla swoich kolorów semantycznych. Skale kolorów Radix Themes są zdefiniowane w OKLCH. Społeczność osiągnęła zbieżność.

Praktyczna migracja: paleta HEX → paleta OKLCH

Jeśli dziś masz paletę opartą na hex, migracja jest mechaniczna. Oto przepis:

1. Zdecyduj o strukturze rampy. 50–950 Tailwinda (11 przystanków) to de facto standard i warto za nim podążać, jeśli nie masz konkretnego powodu, by zrobić inaczej. Przystanki przy L = 0,97, 0,93, 0,86, 0,76, 0,63, 0,50, 0,42, 0,34, 0,26, 0,18, 0,10 dają płynną percepcyjną rampę.

2. Skonwertuj hex marki na OKLCH. Użyj konwertera kolorów albo narzędzia HEX na OKLCH. Dostaniesz trójkę w stylu oklch(0.629 0.193 263.4). Zanotuj wartość H — to jest twoja barwa marki.

3. Utrzymaj C i H stałe; zmieniaj L. Zbuduj rampę, emitując:

--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. Dostrój skrajne przystanki. Przy bardzo niskim L (≤ 0,20) i bardzo wysokim L (≥ 0,95) wysokie wartości chromy wypadają poza sRGB. Zmniejsz C dla tych przystanków albo zaakceptuj automatyczne przyciągnięcie przeglądarki. Domyślne wartości Tailwinda redukują chroma w stronę obu końców — skopiuj ten wzorzec.

5. Zdefiniuj aliasy semantyczne. --surface: var(--brand-50), --surface-elevated: var(--brand-100), --text-primary: var(--brand-900) i tak dalej. Teraz twoje tokeny designu czyta się jako intencję, a nie jako kolory.

6. Zweryfikuj kontrast. Użyj APCA Lc albo współczynników kontrastu WCAG 2, by potwierdzić, że każda para --text-* względem każdej pary --surface-* spełnia twój próg dostępności. Ponieważ L w OKLCH jest percepcyjne, matematyka kontrastu jest bardziej wiarygodna niż byłaby w HSL.

Zespół przeprowadzający tę migrację na 60-kolorowej starszej palecie zwykle ląduje z mniejszą, bardziej jednorodną paletą OKLCH liczącą 30–40 tokenów w ciągu jednego popołudnia. Nowa paleta dostarcza się jako mniejsza, generuje mniejsze CSS i produkuje widocznie lepszy ruch tonalny w stanach hover i stanach wyłączonych bez żadnego dodatkowego strojenia.

Pułapki i jak sobie z nimi radzić

Kilka rzeczy, które warto wiedzieć, wchodząc w to:

Ostrzeżenia o wyjściu poza gamut. Niektóre wartości OKLCH wypadają poza Display P3 albo sRGB. Nowoczesne przeglądarki radzą sobie z przyciągnięciem do najbliższego prawidłowego koloru automatycznie, ale to przyciągnięcie jest stratne: twoje oklch(0.7 0.25 30) może renderować się nieco mniej nasycone niż to, co napisałeś. Narzędzia takie jak wiersz gamutu w konwerterze kolorów informują, czy twój kolor jest bezpieczny dla sRGB, bezpieczny dla P3 czy bezpieczny dla Rec2020, i oferują jednokliknięciowe dopasowanie do sRGB, żeby to, co piszesz, było tym, co widzisz.

Dziwactwa chromy na poziomie subpiksela. Chroma w OKLCH jest nieograniczona, ale użyteczny zakres to z grubsza 0 do ~0,4 dla widzialnych kolorów. Wartości powyżej 0,4 są osiągalne tylko monochromatycznym światłem laserowym — to nie są fizyczne kolory, które wyświetlacz potrafi wyrenderować. Konwerter kolorów ogranicza suwak chromy do 0,4 z tego powodu; wartości poza nim nie produkują żadnej dostrzegalnej różnicy na jakimkolwiek prawdziwym wyświetlaczu.

Wsparcie przeglądarek, w stylu 2025. Chrome 111+, Safari 15.4+ i Firefox 113+ — wszystkie natywnie obsługują oklch(). Przeglądarki sprzed 2023 nie. Jeśli musisz wspierać legacy IE/Edge albo starsze mobilne Safari (1–3% ruchu w zależności od twojej publiczności), możesz sparować deklarację OKLCH z fallbackiem hex za pomocą @supports (color: oklch(0 0 0)) — ale dla tokenów systemu designu dostarczanych w 2025 koszt fallbacku często przewyższa korzyść z obsługi legacy.

Trwałość kodów hex. OKLCH jest dla intencji systemu designu. Twój CMS może nadal potrzebować wartości hex z powodów legacy (podpisy mailowe, dokumenty Office, listy kontrolne assetów marki). Trzymaj wygenerowaną tablicę lookupową, która emituje hex dopasowany do sRGB dla każdego tokenu OKLCH, ale nie pisz w hex u źródła.

Nie myl OKLCH i OKLAB. OKLAB to forma prostokątna (kanały L, a, b); OKLCH to ta sama przestrzeń barw w formie biegunowej (L, C, H). Konwertujesz między nimi jednym krokiem kartezjańsko↔biegunowym. Używaj OKLCH dla tokenów (bardziej czytelne, łatwiejsze do rampowania); używaj OKLAB wewnętrznie, jeśli musisz interpolować albo mieszać kolory.

Wypróbuj na własnej palecie

Najszybszy sposób, by zobaczyć w akcji to, co tu opisaliśmy, to wrzucić hex marki do konwertera kolorów. Wpisz swój kolor marki w pole HEX i odczytaj wyjście OKLCH. Potem przesuń suwaki po stronie OKLCH i obserwuj, jak ta sama barwa pozostaje tą samą barwą, gdy zmniejszasz chroma, oraz jak ta sama jasność pozostaje tą samą jasnością, gdy obracasz barwę. Po kilku minutach będziesz mieć intuicyjne wyczucie, dlaczego HSL od zawsze był złym narzędziem do ramp tonalnych i dlaczego każdy poważny system designu poszedł dalej.

Dla konkretnej konwersji HEX-do-OKLCH użyj narzędzia HEX na OKLCH — ta sama matematyka co w tym artykule, z jedną dodatkową korzyścią: pokazuje klasyfikację gamutu (sRGB / Display P3 / Rec2020), więc wiesz, które z twoich kolorów marki są bezpieczne wszędzie, a które potrzebują urządzenia o szerokim gamucie, by w pełni się wyrenderować.

To jest OKLCH. Warto migrować. Zrobione dobrze, nigdy więcej nie napiszesz hsl().

Powiązane artykuły

Zobacz wszystkie artykuły