Przejdź do głównej zawartości

Kolor

Techniczny: DraftAkceptacja: Approved

Kolor dobieramy przez rolę: color.background.page, color.text.primary, color.brand.primary. Rola mówi, do czego kolor służy; wartość pod spodem zmienia się z marką i trybem, a kod komponentu jej nie zna. Mechanikę warstw opisują tokeny.

Każda marka ma jedną rampę po dziesięć stopni. Stopnia z rampy nie używasz wprost. W kodzie pytasz o rolę, np. color.brand.primary, a rola wskazuje właściwy stopień.

Numer stopnia znaczy w każdej marce to samo. Dlatego kolor marki stoi na 500 wszędzie, a tło przycisku głównego na 700 wszędzie, a nie raz na jednym stopniu, raz na innym.

Marki. Stopień podpisany „marka” to color.brand.primary — ta sama barwa, którą narysowany jest znak. Stopień podpisany „przycisk” to color.action.primary, czyli tło przycisku głównego; hover schodzi na 800, active na 900.

50100200300400500600700800900
insurance#F3F7FB#E3ECF5#C5D6E8#9FB8D6#6F96C2#4A78ADmarka#355F92#274A74przycisk#1A3556#10243D
home#FDF2F4#FADDE3#F6B9C6#F08EA4#E66A84#DE4C6Cmarka#C13A5A#962C46przycisk#6A1E32#471322
health#EAF7F8#CDEDEA#A8DDD7#7CCAC3#52B7AF#2BA09Amarka#248A84#1C6D68przycisk#15524E#0E3A37
moto#F3EEFC#D8CCF7#BFAAF2#A084EA#8B6AE3#7556D8marka#6347C6#5038A3przycisk#3D2A7A#2C1E58
travel#EEF6FF#D6E9FF#B5D8FF#8EC2FA#66A9F0#4C90E3marka#3A78C5#2C5E9Cprzycisk#214574#182F52

Navy. Rampa wspólna: niesie tła, tekst i obrysy we wszystkich pięciu markach. Stopień 50 to jasne tło strony, 900 tekst i ciemne tło strony, 800 powierzchnia trybu ciemnego, 600 drugi poziom tekstu. Biel i czerń stoją osobno, poza rampami. Stopnie 100, 200, 500 i 700 nie niosą dziś żadnej roli i stoją na liście liści nieosiągalnych (tokens/unreachable.json); 700 trafił tam 2026-09-28.

50100200300400500600700800900
wspólna#F8F9FB#F1F3F7#E6EAF0#D2D8E2#AEB6C2#8B95A3#667080#4A5361#1C2430#0B1326

Turkus. Rampa marki insurance obok jej rampy głównej. Na stopniu podpisanym „akcent” stoi color.accent.default tej marki; kilka stopni tej rampy niesie gradient lwa w znaku. Cztery pozostałe marki mają w akcencie kolor własnego znaku, więc własnej rampy akcentu nie mają.

50100200300400500600700800900
akcent insurance#EEFBFA#DCF4F2#BAE8E5#8FD5D6#60BBC9#3F9BB7akcent#3183A4#266888#1B4D68#0C334A

Kolor znaku. Znak marki stoi na stopniu 500 rampy — tym samym, który niesie color.brand.primary. brand.mark jest nazwą roli dla tej barwy w warstwie marki, nie osobną wartością: zapisuje fakt o logotypie, a stopień 500 zapisuje ten sam fakt o rampie. Nie sięga po brand.mark żadna rola semantyczna: pliki znaku wydaje osobna paczka. W interfejsie kolorem marki jest color.brand.primary.

CSS własny
.card {
background: var(--color-background-surface);
color: var(--color-text-primary);
border: var(--border-width-hairline) solid var(--color-border-strong);
}
Klasa narzędziowa Tailwind — ta sama wartość
<div className="bg-background-surface text-text-primary border border-border-strong">…</div>

Rampy bazowe (blue-600, navy-500) nie istnieją jako publiczny kontrakt i nie pojawiają się w kodzie pod żadną postacią.

Próbka pokazuje wartość roli w marce insurance: lewy kwadrat to tryb jasny, prawy ciemny.

Stos teł ma dwa poziomy: background.page i background.surface. Rola background.raised zniknęła 2026-09-28 — miała identyczną wartość co surface we wszystkich dziesięciu kombinacjach. Uniesienie niesie cień (tryb jasny) i obrys (tryb ciemny), nie stopień koloru — reguła i pomiary stoją w powierzchniach.

PodglądRola (token)Zmienna CSSKlasa TailwindHex — lightHex — dark
color.background.page--color-background-pagebg-background-page#F8F9FB#0B1326
color.background.surface--color-background-surfacebg-background-surface#FFFFFF#1C2430
color.background.inverse--color-background-inversebg-background-inverse#0B1326#FFFFFF
color.text.primary--color-text-primarytext-text-primary#0B1326#F8F9FB
color.text.secondary--color-text-secondarytext-text-secondary#667080#AEB6C2
color.text.muted--color-text-mutedtext-text-muted#667080#AEB6C2
color.text.inverse--color-text-inversetext-text-inverse#FFFFFF#0B1326
color.text.on-action--color-text-on-actiontext-text-on-action#FFFFFF#FFFFFF
color.text.link--color-text-linktext-text-link#274A74#9FB8D6
color.brand.primary--color-brand-primarybg-brand-primary#4A78AD#4A78AD
color.action.primary--color-action-primarybg-action-primary#274A74#274A74
color.action.hover--color-action-hoverbg-action-hover#1A3556#1A3556
color.action.active--color-action-activebg-action-active#10243D#10243D
color.brand.subtle--color-brand-subtlebg-brand-subtle#E3ECF5#1A3556
color.brand.on-subtle--color-brand-on-subtletext-brand-on-subtle#1A3556#C5D6E8
color.accent.default--color-accent-defaultbg-accent-default#3F9BB7#3F9BB7
color.border.default--color-border-defaultborder-border-default#D2D8E2#667080
color.border.strong--color-border-strongborder-border-strong#667080#AEB6C2
color.border.focus--color-border-focusborder-border-focus#355F92#6F96C2
color.status.success.bg--color-status-success-bgbg-status-success-bg#F5F9F7#F5F9F7
color.status.success.fg--color-status-success-fgtext-status-success-fg#0E7D36#0E7D36
color.status.warning.bg--color-status-warning-bgbg-status-warning-bg#FAF8F5#FAF8F5
color.status.warning.fg--color-status-warning-fgtext-status-warning-fg#966106#966106
color.status.danger.bg--color-status-danger-bgbg-status-danger-bg#FAF5F5#FAF5F5
color.status.danger.fg--color-status-danger-fgtext-status-danger-fg#D81212#D81212
color.status.danger.surface--color-status-danger-surfacebg-status-danger-surface#D81212#D81212
color.status.danger.on-surface--color-status-danger-on-surfacetext-status-danger-on-surface#FFFFFF#FFFFFF
color.status.danger.hover--color-status-danger-hoverbg-status-danger-hover#AD0E0E#AD0E0E
color.status.danger.text--color-status-danger-texttext-status-danger-text#D81212#F15656
color.status.info.bg--color-status-info-bgbg-status-info-bg#F2F5F8#F2F5F8
color.status.info.fg--color-status-info-fgtext-status-info-fg#1E62AE#1E62AE

data-brand × data-theme na <html> daje 10 kombinacji tych samych nazw. Tryb ciemny to przemapowanie ról, nie druga paleta, więc komponent napisany raz działa w obu. Podgląd na żywo w wybranej marce i trybie: strony /preview/* mają własny przełącznik.

insurance.lightinsurance.darkhome.lighthome.darkhealth.lighthealth.darkmoto.lightmoto.darktravel.lighttravel.dark
color.background.page
color.background.surface
color.background.inverse
color.text.primary
color.text.secondary
color.text.muted
color.text.on-action
color.brand.primary
color.action.primary
color.action.hover
color.action.active
color.brand.subtle
color.accent.default
color.border.default
color.border.strong
color.status.success.fg
color.status.warning.fg
color.status.danger.fg
color.status.info.fg

tokens/src/artifact.ts liczy WCAG 2.2 AA dla listy par, które komponenty naprawdę renderują: 4,5:1 dla tekstu, 3:1 dla obrysu i pierścienia focus. Każda para z listy jest liczona w każdej z 10 kombinacji. To nie jest iloczyn kartezjański ról. Zestawienie spoza listy nie jest sprawdzone i nie wolno zakładać, że przechodzi.

color.border.default jest poza bramką celowo: to dekoracyjny separator, a nie granica komponentu w rozumieniu WCAG 1.4.11. Granicę pola, karty i przycisku secondary niesie color.border.strong, liczony na background.page i background.surface.

Nową parę tekst/tło dopisujesz do listy, zanim komponent jej użyje. Tak powstały color.status.danger.text (kolor błędu renderowany wprost na tle strony) i color.brand.on-subtle (tekst na brand.subtle): odcień, który wystarczał jako plama, nie wystarczał jako tekst. Gdy odcień nie przechodzi progu, poprawka wchodzi w paletę marki — źródło w repo: packages/design/tokens/src/brands/insurance.tokens.json, nigdy w próg bramki.

Role są projektowane w parach. color.text.on-action idzie na color.action.*, color.status.danger.on-surface na color.status.danger.surface. Zestawienie ról z różnych rodzin to decyzja do tokenów, nie do lokalnego CSS.

Zakres kontroli dostępności w całym obszarze: standard dostępności.

Akcent jest kolorem towarzyszącym, nie drugą marką. Ma jedną rolę i jedną wartość na markę, tę samą w obu trybach: color.accent.default. Insurance ma w niej turkus, pozostałe cztery marki mają własny kolor znaku.

Akcent wolno obciążyć informacją: bramka mierzy go na background.page i background.surface w progu 3:1 dla elementu nietekstowego (WCAG 2.2 SC 1.4.11), w każdej z 10 kombinacji marki i trybu. Żeby próg wszędzie przechodził, dwie wartości przyciemniono 2026-09-27 — akcent health i turkus insurance. Powód i zakres zmiany (razem z plikami znaku) stoją w paczce znaku — źródło w repo: packages/design/brand/README.md.

Próg mierzy bramka na obu podłożach stosu teł: background.page i background.surface. Karta stoi na surface, więc pomiar karty jest tym samym pomiarem; najciaśniejszy wynik to 3,02:1 (health, tryb jasny, na background.page). Nowa powierzchnia w tokenach oznacza nowy pomiar akcentu i nową parę w bramce, zanim obciąży się go informacją.

Granicy kontrolki akcentem nie rysujemy: od niej jest color.border.strong. Plama, ilustracja, wykres, wyróżnienie — tak; obrys pola i pierścień focus — nie.

Akcent nie jest kolorem tekstu. Nie zastępuje przycisku głównego (color.action.primary) ani linku (color.text.link). Kolorowy tekst na akcencie wymaga najpierw własnego tokenu i wpisu do bramki, wzorem brand.on-subtle.

success, warning, danger i info nie zależą od marki: status nie jest kolorem marki, więc w pięciu portalach wartość jest ta sama. Każdy niesie parę fg/bg przechodzącą 4,5:1, a danger dodatkowo cztery role dla wypełnienia i dla tekstu wprost na tle — surface, on-surface, hover i text.

Od trybu zależy dokładnie jedna z tych ról: status.danger.text, czyli napis błędu i obrys nieprawidłowej kontrolki wprost na tle. W trybie jasnym niesie #D81212, w ciemnym #F15656. To nie wyjątek od reguły, tylko jej skutek: wartość statusu wynika z wymaganego kontrastu, a stała #D81212 daje na powierzchni trybu ciemnego 2,99:1 — poniżej 3:1 wymaganych dla granicy kontrolki (WCAG 2.2 SC 1.4.11). Pozostałe role statusów, razem z danger.fg, danger.surface, danger.on-surface i danger.hover, są identyczne we wszystkich dziesięciu kombinacjach.

Koloru statusu nie dobierasz ręcznie i nie rozjaśniasz „żeby ładniej wyglądał”: wartość wynika z wymaganego kontrastu.

Barwa z dostawy. Obok pary fg/bg paleta niesie source — nasyconą barwę z projektu, czytelną jako plama i jako ikona. Stoi zawsze obok etykiety i tekstu, jako ich wzmocnienie, nigdy jako jedyny nośnik stanu, więc nie podlega progowi elementu nietekstowego (WCAG 2.2 SC 1.4.11): stan nazywa napis, barwa tylko prowadzi do niego wzrok. Warunek jest twardy. Status info barwy source nie ma: projekt źródłowy jej nie podaje. Gdy plama ma stanąć sama — bez etykiety, bez tekstu, jako jedyny znak stanu — próg 3:1 zaczyna ją obowiązywać i wartość trzeba wtedy przyciemnić, tak jak przyciemniony jest fg. Napisu source nie niesie: kolorem tekstu statusu jest fg.

  • Kolor nie niesie informacji sam. Status ma etykietę i tekst, nie tylko barwę.
  • Nie dobieramy odcienia w CSS komponentu. Brakująca rola to zmiana w tokenach.
  • Tryb ciemny nie jest osobnym wariantem komponentu. Komponent czyta te same role.
  • Marka nie rozgałęzia kodu. Różnicę niesie wartość tokenu, nie warunek w JSX.
  1. pnpm -F @polisoteka/design-tokens build && pnpm -F @polisoteka/design-tokens check:tokens kończy się zielono. Tabele na tej stronie czytają dist/tokens.json, więc po przebudowaniu pokazują nową wartość bez ręcznej poprawki.
  2. Każde nowe zestawienie tekst/tło ma wpis w liście par bramki.
  3. Komponent przełącza markę i tryb bez zmiany kodu. Sprawdzasz to na /preview/*, w obu trybach.
  4. pnpm design:audit przechodzi, łącznie z kontrolą axe i wartościami policzonymi w przeglądarce w 10 kombinacjach.

Tokeny i zależności, Język wizualny marki, Standard dostępności, README paczki tokenów — źródło w repo: packages/design/tokens/README.md, bramka kontrastu — źródło w repo: packages/design/tokens/src/artifact.ts.