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.
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.
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.
- 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).
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.
Zagłębione obszary, które obramowują treść: paski boczne, wnęki, kod.
Wyniesione powierzchnie: karty, panele, kafelki.
Powierzchnie pływające: menu, popovery, podpowiedzi, okna dialogowe.
Domyślny tekst i ikony.
Akcje
Jedna, najważniejsza akcja. Domyślnie monochromatyczna.
Akcje pomocnicze i ciche wypełnienia.
Wyciszone wypełnienia: ścieżki, szkielety, szyny zakładek.
Wypełnienie najechania i wyróżnienia elementów list i menu.
Tekst drugorzędny, opisy, placeholdery. Spełnia 4,5:1.
Marka
Kolor marki. Zaznaczenie, fokus, postęp i stany zaznaczone. Monochromatyczny, dopóki motyw go nie ustawi.
Drugi kolor marki, do wyróżnień, które wybierzesz. Monochromatyczny, dopóki motyw go nie ustawi.
Linki w tekście.
Podświetlenie zaznaczonego tekstu.
Wskaźnik fokusu klawiatury. Spełnia 3:1 na każdej powierzchni.
Status
Akcje nieodwracalne i błędy. Wypełnienia używają danger-9, tekst — danger-11.
Pozytywne wyniki.
Ostrzeżenie. Zawsze z ciemnym tekstem.
Neutralna informacja.
Linie
Obramowania strukturalne: karty, separatory, tabele. Dekoracyjne.
Podkreślona struktura i obrysy po najechaniu.
Granice kontrolek formularzy. Spełnia 3:1 względem strony (WCAG 1.4.11).
Włoskowaty pierścień wokół powierzchni pływających (menu, popovery, okna dialogowe).
Przyciemnienie za powierzchniami modalnymi.
Stany
Wypełnienie secondary po najechaniu; wciśnięte przełączniki.
Wypełnienie secondary po naciśnięciu; wyróżnione elementy na wyciszonej powierzchni.
Granica kontrolki formularza po najechaniu.
Obrys kontrolki z obramowaniem po najechaniu lub naciśnięciu.
Wypełnienie w kolorze marki po najechaniu.
Dane
Seria 1.
Seria 2.
Seria 3.
Seria 4.
Seria 5.
// 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ć:
redambergreenblueimport {
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.
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.
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
- 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.
export const huePresets = {
ultramarine: { hue: 267, chroma: 0.24, lightness: 0.515, darkLightness: 0.55 },
// …
}