Przejdź do treści

Kolor

Zamknięta, generowana paleta w OKLCH. Dwanaście stopni na każdy odcień, jedno zadanie na stopień i kontrast, który wynika z obliczeń — nie z nadziei.

System kolorów prfct jest celowo mały. Nie ma tu blue-500, po który można by sięgnąć, ani kodu hex do wklejenia. Każdy kolor w produkcie pochodzi z 12-stopniowej skali, a każdy stopień istnieje po to, żeby wykonywać jedno konkretne zadanie. Wybierz zadanie, a stopień wybierze się sam.

Podgląd wdrożeniaGotowe
x
import { Badge, BadgeDot } from "@/components/ui/badge"
import { Button } from "@/components/ui/button"
import { Checkbox } from "@/components/ui/checkbox"
import { Label } from "@/components/ui/label"
import { Progress } from "@/components/ui/progress"
import { Switch } from "@/components/ui/switch"

export default function MeaningfulColor() {
  return (
    <div className="flex w-full max-w-sm flex-col gap-5 rounded-xl border bg-card p-5 shadow-raised">
      <div className="flex items-center justify-between">
        <span className="text-sm font-semibold">Podgląd wdrożenia</span>
        <Badge variant="success">
          <BadgeDot />
          Gotowe
        </Badge>
      </div>
      <Progress value={72} aria-label="Postęp kompilacji" />
      <div className="flex items-center gap-2.5">
        <Checkbox id="notify" defaultChecked />
        <Label htmlFor="notify">Powiadom mnie po publikacji</Label>
      </div>
      <div className="flex items-center gap-2.5">
        <Switch id="auto" defaultChecked />
        <Label htmlFor="auto">Publikuj automatycznie</Label>
      </div>
      <div className="flex justify-end gap-2">
        <Button variant="outline" size="sm">
          Pokaż logi
        </Button>
        <Button size="sm">Opublikuj</Button>
      </div>
    </div>
  )
}

Zasady

Domyślnie monochromatycznie. Powierzchnie, tekst i główne akcje są neutralne. Na spokojnym płótnie nieliczne kolorowe elementy mogą nieść znaczenie, zamiast walczyć o uwagę.

Kolor to sygnał, nie dekoracja. prfct używa barwy tylko do trzech zadań:

  • Marka oznacza stan i zaznaczenie: zaznaczone pola wyboru, włączone przełączniki, postęp, pierścienie fokusu, linki. Jest monochromatyczna, dopóki motyw nie nada jej koloru.
  • Statusy komunikują wyniki: sukces, ostrzeżenie, błąd i informację.
  • Odcienie kategorialne rozróżniają serie na wykresach oraz tagi — nigdy nie niosą znaczenia.

Nigdy sam kolor. Każdy stan oznaczony kolorem jest też oznaczony kształtem, ikoną albo tekstem, więc pozostaje czytelny przy zaburzeniach widzenia barw, w wydruku w skali szarości i w trybie wymuszonych kolorów (WCAG 1.4.1).

Skale

prfct ma jedenaście skal: neutralną gray, dwie skale marki (brand i brand-secondary), które dostosujesz w motywie, cztery odcienie statusów i cztery odcienie kategorialne. Poniżej oba tryby, wyrenderowane z tego samego arkusza stylów.

123456789101112
gray
brand
brand-secondary
red
amber
green
blue
orange
teal
violet
pink
123456789101112
gray
brand
brand-secondary
red
amber
green
blue
orange
teal
violet
pink

Kliknij dowolną próbkę, aby skopiować jej token. Oba tryby są renderowane na żywo z tego samego CSS: każdy panel to poddrzewo z klasą .light lub .dark.

Skale są generowane w OKLCH — percepcyjnie jednorodnej przestrzeni barw, w której równe przyrosty jasności wyglądają na równe w każdym odcieniu. Dlatego bursztyn ostrzeżenia i błękit informacji na tym samym stopniu mają tę samą wagę wizualną — a tego HSL nie potrafi.

Anatomia skali

Każdy z dwunastu stopni ma jedną rolę. Komponenty nigdy nie dobierają stopni na oko — wybierają je według roli.

1
2
3
4
5
6
7
8
9
10
11
12
Tła
1 · 2
Płótno aplikacji i subtelne powierzchnie, takie jak paski boczne, wnęki i paskowane wiersze.
Wypełnienia komponentów
3 · 4 · 5
Wypełnienia elementów interaktywnych: spoczynkowe, po najechaniu oraz wciśnięte lub zaznaczone.
Obramowania
6 · 7 · 8
Subtelne separatory, domyślne obramowania komponentów oraz obramowania mocne lub po najechaniu.
Wypełnienia pełne
9 · 10
Najczystsza postać barwy, do wypełnionych przycisków, odznak i wskaźników, oraz jej stan po najechaniu.
Tekst
11 · 12
Tekst o niższym kontraście (≥ 4,5:1 na każdym wypełnieniu, stopnie 1–5) i tekst o wysokim kontraście (≥ 11:1).
StopnieRolaTypowe klasy
1–2Tło aplikacji i subtelne tłabg-gray-1, bg-brand-2
3–5Wypełnienia komponentów — spoczynkowe, po najechaniu, wciśniętebg-gray-3 hover:bg-gray-4 active:bg-gray-5
6–8Obramowania — subtelne, domyślne, mocneborder-gray-6, hover:border-gray-8
9–10Wypełnienia pełne i ich stan po najechaniubg-brand-9 hover:bg-brand-10
11Tekst o niższym kontraście, ikony, linki — AA na stopniach 1–5text-brand-11
12Tekst o wysokim kontraścietext-gray-12

Tokeny semantyczne

Przez większość czasu stopni skali w ogóle nie dotkniesz. Tokeny semantyczne nazywają przeznaczenie koloru i w każdym trybie wskazują na konkretny stopień. Ich nazwy trzymają się konwencji shadcn/ui — background, primary, muted-foreground — dzięki czemu każdy komponent zgodny z shadcn działa z prfct bez zmian.

Powierzchnie

Płótno strony.

whitegray-1

Zagłębione obszary, które obramowują treść: paski boczne, wnęki, kod.

gray-2gray-2

Wyniesione powierzchnie: karty, panele, kafelki.

whitegray-2

Powierzchnie pływające: menu, popovery, podpowiedzi, okna dialogowe.

whitegray-3

Domyślny tekst i ikony.

gray-12gray-12

Akcje

Jedna, najważniejsza akcja. Domyślnie monochromatyczna.

gray-12gray-12

Akcje pomocnicze i ciche wypełnienia.

gray-3gray-3

Wyciszone wypełnienia: ścieżki, szkielety, szyny zakładek.

gray-3gray-3

Wypełnienie najechania i wyróżnienia elementów list i menu.

gray-3gray-4

Tekst drugorzędny, opisy, placeholdery. Spełnia 4,5:1.

gray-11gray-11

Marka

Kolor marki. Zaznaczenie, fokus, postęp i stany zaznaczone. Monochromatyczny, dopóki motyw go nie ustawi.

brand-9brand-9

Drugi kolor marki, do wyróżnień, które wybierzesz. Monochromatyczny, dopóki motyw go nie ustawi.

brand-secondary-9brand-secondary-9

Linki w tekście.

brand-11brand-11

Podświetlenie zaznaczonego tekstu.

brand-5brand-6

Wskaźnik fokusu klawiatury. Spełnia 3:1 na każdej powierzchni.

brand-focusbrand-focus

Status

Akcje nieodwracalne i błędy. Wypełnienia używają danger-9, tekst — danger-11.

red-9red-10

Pozytywne wyniki.

green-9green-9

Ostrzeżenie. Zawsze z ciemnym tekstem.

amber-9amber-9

Neutralna informacja.

blue-9blue-9

Linie

Obramowania strukturalne: karty, separatory, tabele. Dekoracyjne.

gray-6gray-6

Podkreślona struktura i obrysy po najechaniu.

gray-7gray-7

Granice kontrolek formularzy. Spełnia 3:1 względem strony (WCAG 1.4.11).

gray-9gray-9

Włoskowaty pierścień wokół powierzchni pływających (menu, popovery, okna dialogowe).

gray-12 @ 8%gray-12 @ 12%

Przyciemnienie za powierzchniami modalnymi.

gray-12 @ 32%black @ 60%

Stany

Wypełnienie secondary po najechaniu; wciśnięte przełączniki.

gray-4gray-4

Wypełnienie secondary po naciśnięciu; wyróżnione elementy na wyciszonej powierzchni.

gray-5gray-5

Granica kontrolki formularza po najechaniu.

gray-10gray-10

Obrys kontrolki z obramowaniem po najechaniu lub naciśnięciu.

gray-8gray-8

Wypełnienie w kolorze marki po najechaniu.

brand-10brand-10

Dane

Seria 1.

brand-9brand-10

Seria 2.

teal-9teal-9

Seria 3.

violet-9violet-10

Seria 4.

orange-9orange-9

Seria 5.

pink-9pink-10
// Reach for semantic tokens first…
<p className="text-muted-foreground">Last edited 2 hours ago</p>

// …and scale steps when you're building a component that needs a specific role.
<span className="bg-brand-3 text-brand-11">Beta</span>

Marka

prfct jest czarno-biały: skala marki podąża za skalą szarości, więc zaznaczenie, postęp i fokus są czarne, a w trybie ciemnym białe. Ta powściągliwość jest celowa. Gdy nic innego nie ma koloru, koloru statusu nie da się przeoczyć.

Produkty wnoszą własny kolor. Motyw ustawia do dwóch kolorów marki, główny i drugi, z gotowej palety, z wartości hex albo z trzech liczb, a wszystkie dwanaście stopni każdego z nich powstaje na nowo razem z gwarancjami kontrastu. Kolory statusów zostają na swoim miejscu, więc sukces nigdy nie przybierze koloru twojej marki. Zobacz Motywy i kreator motywów.

Kolory statusów

Odcienie statusów mają aliasy semantyczne, dzięki czemu komponenty mówią, co się stało, a nie jakim kolorem to namalować:

danger-1…12Alias skali red
warning-1…12Alias skali amber
success-1…12Alias skali green
info-1…12Alias skali blue
Wdrożonosuccess-2 · 6 · 9 · 11 · 12
Obniżona wydajnośćwarning-2 · 6 · 9 · 11 · 12
Niepowodzeniedanger-2 · 6 · 9 · 11 · 12
W kolejceinfo-2 · 6 · 9 · 11 · 12
import {
  CircleCheckIcon,
  CircleXIcon,
  InfoIcon,
  TriangleAlertIcon,
} from "lucide-react"

// Status aliases resolve to hue scales: success → green, warning → amber,
// danger → red, info → blue.
const statuses = [
  { name: "success", scale: "green", label: "Wdrożono", icon: CircleCheckIcon },
  {
    name: "warning",
    scale: "amber",
    label: "Obniżona wydajność",
    icon: TriangleAlertIcon,
  },
  { name: "danger", scale: "red", label: "Niepowodzenie", icon: CircleXIcon },
  { name: "info", scale: "blue", label: "W kolejce", icon: InfoIcon },
] as const

export default function StatusColors() {
  return (
    <div className="grid w-full max-w-2xl gap-3 sm:grid-cols-2">
      {statuses.map(({ name, scale, label, icon: Icon }) => (
        <div
          key={name}
          className="flex items-center gap-3 rounded-xl border p-3"
          style={{
            backgroundColor: `var(--${scale}-2)`,
            borderColor: `var(--${scale}-6)`,
          }}
        >
          <span
            className="flex size-9 items-center justify-center rounded-lg"
            style={{
              backgroundColor: `var(--${scale}-9)`,
              color: `var(--${scale}-contrast)`,
            }}
          >
            <Icon className="size-4" aria-hidden="true" />
          </span>
          <div className="flex flex-col">
            <span
              className="text-sm font-medium"
              style={{ color: `var(--${scale}-12)` }}
            >
              {label}
            </span>
            <span
              className="font-mono text-xs"
              style={{ color: `var(--${scale}-11)` }}
            >
              {name}-2 · 6 · 9 · 11 · 12
            </span>
          </div>
        </div>
      ))}
    </div>
  )
}

Wszędzie stosuj ten sam przepis: stopień 2 na tło, 6 na obramowanie, 9 na pełne wypełnienie (z -contrast jako kolorem tekstu), 11 na tekst i ikony, 12 na tytuł na zabarwionym tle. Alerty stosują cały przepis. Odznaki mają wypełnienie ze stopnia 3, czyli wypełnienia komponentu, pod tekstem ze stopnia 11. Błędy w formularzach używają stopnia 11 dla ikony i tekstu, a powiadomienia tylko dla ikony.

Płatność nieudana
DobrzeTekst statusu używa stopnia 11, który ma wyliczony kontrast AA na zabarwionych tłach własnej skali.
Płatność nieudana
ŹlePełny stopień 9 jako kolor tekstu nie ma wystarczającego kontrastu na zabarwionych tłach, zwłaszcza w trybie ciemnym.

Kontrast

W prfct kontrast jest cechą palety, a nie etapem przeglądu. Kiedy skala powstaje, jej stopnie tekstu są wyliczane numerycznie względem jej wypełnień:

  • Stopień 11 osiąga co najmniej 4,5:1 na każdym wypełnieniu własnej skali — na stopniach od 1 do 5, łącznie ze stanami najechania i wciśnięcia — co odpowiada poziomowi AA w WCAG 2.2 dla tekstu podstawowego. W trybie ciemnym osiąga też 5:1 na stopniu 3, bo przez halację cienki jasny tekst na ciemnym tle czyta się trudniej.
  • Stopień 12 osiąga co najmniej 11:1 na stopniu 3.
  • Każde wypełnienie pełne (stopień 9) ma kolor tekstu -contrast, który spełnia 4,5:1: biały, gdzie to możliwe, a ciemny odcień tej samej barwy tam, gdzie biały nie wystarcza (bursztyn i pomarańcz).
  • Granice kontrolek formularzy używają tokenu input, który spełnia 3:1 względem strony zgodnie z WCAG 1.4.11 — to element, który większość design systemów po cichu pomija.

Poniższa macierz jest liczona z wygenerowanej palety podczas budowania. Gdyby jakakolwiek zmiana naruszyła gwarancję, pokazałaby to tabela — i testy.

Skala11 na 111 na 211 na 311 na 411 na 512 na 3Kontrast na 9
gray5,86AA5,65AA5,31AA4,97AA4,63AA15,0Lc 965,38AA
gray9,32AA8,81AA8,17AA7,53AA6,88AA14,8Lc 965,28AA
brand16,59AA15,98AA15,02AA14,07AA13,11AA15,0Lc 9616,98AA
brand16,92AA15,99AA14,83AA13,67AA12,48AA14,8Lc 9616,92AA
brand-secondary16,59AA15,98AA15,02AA14,07AA13,11AA15,0Lc 9616,98AA
brand-secondary16,92AA15,99AA14,83AA13,67AA12,48AA14,8Lc 9616,92AA
red6,00AA5,78AA5,44AA5,05AA4,60AA11,9Lc 914,93AA
red7,70AA7,32AA6,43AA5,69AA4,91AA12,1Lc 864,93AA
amber5,93AA5,73AA5,41AA5,04AA4,60AA11,7Lc 919,66AA
amber8,02AA7,60AA6,58AA5,74AA4,92AA12,0Lc 869,66AA
green5,82AA5,64AA5,33AA5,00AA4,60AA11,3Lc 914,74AA
green8,51AA8,04AA6,87AA5,91AA5,01AA12,0Lc 874,74AA
blue5,89AA5,70AA5,38AA5,02AA4,60AA11,6Lc 914,70AA
blue8,20AA7,76AA6,71AA5,85AA5,00AA12,0Lc 874,70AA
orange5,98AA5,77AA5,43AA5,05AA4,60AA11,8Lc 915,56AA
orange7,82AA7,42AA6,50AA5,71AA4,92AA12,1Lc 865,56AA
teal5,84AA5,65AA5,34AA5,01AA4,60AA11,4Lc 914,60AA
teal8,43AA7,97AA6,81AA5,87AA4,99AA12,0Lc 874,60AA
violet5,97AA5,76AA5,42AA5,05AA4,60AA11,9Lc 925,72AA
violet7,83AA7,43AA6,52AA5,76AA4,97AA12,1Lc 865,72AA
pink6,03AA5,81AA5,46AA5,07AA4,60AA11,9Lc 914,67AA
pink7,69AA7,30AA6,42AA5,68AA4,91AA12,1Lc 864,67AA
Zwiększony kontrast
Gdy system operacyjny prosi o większy kontrast (prefers-contrast: more), prfct automatycznie wzmacnia obramowania, granice kontrolek i tekst drugorzędny. W trybie dużego kontrastu w Windows (forced-colors: active) komponenty oddają pierwszeństwo kolorom systemowym, a pierścienie fokusu pozostają widoczne, bo są rysowane obrysem, a nie cieniem.

Tryb ciemny

Tryb ciemny to nie odwrócenie kolorów. Każda skala jest generowana dwukrotnie, z osobnymi krzywymi jasności i nasycenia: ciemne tła są prawie czarne, a nie czysto czarne, żeby uniknąć smużenia na ekranach OLED; zabarwione stopnie mają większe nasycenie, żeby przy niskiej jasności wciąż było w nich widać kolor; a stopnie tekstu są wyliczane względem ciemnych wypełnień. Powierzchnie zyskują elewację, stając się jaśniejsze, a nie rzucając cieni, których nikt nie zobaczy.

Dowolne poddrzewo może przełączyć tryb przez dodanie klasy — to przydatne w podglądach, osadzonych panelach i sekcjach marketingowych:

<section className="dark">…</section>   {/* dark, even on a light page */}
<section className="light">…</section>  {/* light, even on a dark page */}

Reguły

mutedbrand-3/11
DobrzeTokeny semantyczne i stopnie skali — paleta pozostaje spójna i gotowa na zmianę motywu.
#6366f1
ŹleSurowe kody hex i wartości arbitralne psują tryb ciemny, motywy i gwarancje kontrastu.
  • Nie polegaj na samym kolorze. Łącz kolory statusów z ikoną albo tekstem.
  • Nie wymyślaj stopni. Jeśli wydaje się, że brakuje jakiegoś koloru, najpewniej któraś rola jest używana niezgodnie z przeznaczeniem — wróć do tabeli powyżej.
  • Nie przekolorowuj komponentów przez className. Użyj wariantu albo dodaj nowy do komponentu, żeby ta decyzja była wspólna.

Dla programistów

Paleta to dane. Skale są opisane kilkoma liczbami w lib/tokens/palette.ts i generowane przez lib/color/scale.ts; pnpm tokens zapisuje styles/tokens.css oraz JSON w formacie DTCG dla narzędzi projektowych. Domyślna paleta Tailwinda jest usunięta, więc przypadkowe bg-blue-500 po prostu nie istnieje, zamiast po cichu odbiegać od systemu.

lib/tokens/palette.ts
export const huePresets = {
  ultramarine: { hue: 267, chroma: 0.24, lightness: 0.515, darkLightness: 0.55 },
  // …
}