Przejdź do głównej zawartości

Typografia

Techniczny: StableAkceptacja: Approved

Jeden krój tekstowy i trzynaście poziomów skali obsługują całą markę: ekrany, dokumenty i materiały. Poziom wybierasz gotową klasą text-<rola>, która niesie rozmiar, interlinię i wagę naraz.

Krojem marki jest Geist. W CSS rodzina nazywa się Geist Sans, bo pod taką nazwą rejestruje ją @fontsource/geist-sans, z którego serwujemy webfont lokalnie, bez CDN. Plik fontu niesie komplet polskich znaków diakrytycznych, więc nie potrzebujemy osobnego subsetu.

Kroje dostarcza paczka ui plikiem @polisoteka/design-ui/styles/fonts.css. Importuje go globals.css paczki, więc konsument dostaje kroje razem z resztą stylów. Styleguide importuje ten sam plik, nie własną listę wag.

Wartość tokenu font.family.sans
"Geist Sans", system-ui, -apple-system, "Segoe UI", sans-serif
Użycie
.doc {
font-family: var(--font-family-sans);
}

Dalsza część stosu to kroje systemowe. Przejmują tekst, zanim webfont się wczyta, i wtedy, gdy się nie wczyta wcale. Serwujemy trzy wagi i tylko tych trzech używa skala: 400 w tekście i podpisach, 500 w etykietach, 600 w nagłówkach. Waga spoza tej trójki daje pogrubienie syntetyczne, nie odmianę kroju.

Pogrubienie w treści (strong, b, dt, th) bierze 600 z tokenu font.weight.strong. Bez tej reguły przeglądarka dawała mu 700, którego nie serwujemy. Element z klasą text-<rola> bierze wagę z poziomu, nie z tej reguły.

Kod, identyfikatory i wartości techniczne biorą var(--font-family-mono), czyli Geist Mono z tej samej rodziny. Serwujemy dwie wagi: 400 w tekście i 600, bo kod stoi też w pogrubieniu i w nagłówkach. W styleguide ten sam stos niesie kod w treści i bloki kodu.

Wartość tokenu font.family.mono
"Geist Mono", ui-monospace, SFMono-Regular, Menlo, Consolas, monospace

Skala ma trzynaście poziomów. Cztery największe (display-l, display, heading-l, heading-m) mają własny rozmiar i interlinię na mobile; pozostałe dziewięć jest identyczne na obu szerokościach. Podgląd renderuje tę samą klasę, którą dostaje kod.

PodglądRolaKlasaDesktop (size / line-height / weight)Mobile
Polisoteka®display-ltext-display-l4rem / 1.125 / 6003rem / 1.1667 / 600
Polisoteka®displaytext-display3.5rem / 1.2143 / 6002.5rem / 1.2 / 600
Polisoteka®heading-ltext-heading-l2.5rem / 1.2 / 6002rem / 1.25 / 600
Polisoteka®heading-mtext-heading-m2rem / 1.25 / 6001.75rem / 1.2857 / 600
Polisoteka®heading-stext-heading-s1.5rem / 1.3333 / 600= desktop
Polisoteka®heading-xstext-heading-xs1.25rem / 1.4 / 600= desktop
Polisoteka®body-ltext-body-l1.125rem / 1.6667 / 400= desktop
Polisoteka®body-mtext-body-m1rem / 1.625 / 400= desktop
Polisoteka®body-stext-body-s0.875rem / 1.5714 / 400= desktop
Polisoteka®label-ltext-label-l1.125rem / 1.5556 / 500= desktop
Polisoteka®labeltext-label1rem / 1.5 / 500= desktop
Polisoteka®label-stext-label-s0.875rem / 1.4286 / 500= desktop
Polisoteka®captiontext-caption0.75rem / 1.3333 / 400= desktop

Dwanaście poziomów przychodzi z arkusza grafika, z nazwami 1:1. Trzynasty, label-l, dopisaliśmy decyzją Human: arkusz niesie etykietę w dwóch stopniach (16 i 14 px), a duży przycisk potrzebuje trzeciego. Rozmiar jest ten sam co w body-l, bo tyle napis dużego przycisku miał od początku; różni je waga, 500 zamiast 400. Interlinia wynika z logiki rodziny: pudełko wiersza stoi na siatce 4 px i rośnie o 4 px na stopień, więc po 20 i 24 px idzie 28 px.

Tak
<h1 className="text-heading-l text-text-primary">Ubezpieczenie OC dla programisty</h1>
<p className="text-body-m text-text-secondary">Co obejmuje i ile kosztuje.</p>
Nie tak
// rozmiar z jednego poziomu, waga z innego: to już nie jest poziom skali
<p className="text-body-m font-semibold">…</p>

Rozmiar, interlinia i waga są w dist/css/tokens.css osobnymi zmiennymi, bo build nie emituje kompozytu jako jednej wartości. Klasa text-<rola> scala je z powrotem i jest jedynym miejscem, gdzie ta trójka się spotyka. Potrzeba mocniejszego akcentu w tekście oznacza inny poziom, nie inną wagę na tym samym. Jedynym wyjątkiem jest wyróżnienie słowa w zdaniu: zapisujesz je elementem <strong>, który bierze font.weight.strong, a nie klasą.

Kontrprzykładu nie da się już skompilować. dist/theme.css gasi domyślny motyw Tailwinda także w przestrzeniach typografii — --font-weight-*, --text-*, --leading-* i --tracking-* — więc font-semibold, font-medium, text-sm i tracking-tight nie mają żadnej wartości do wzięcia i Tailwind nie generuje dla nich klasy. Waga fontu przychodzi z poziomu skali. Token font.weight.strong też nie daje klasy: font-strong się nie kompiluje, a pilnuje tego bramka tailwind-namespaces.

PoziomGdzie
display-l, displayHero startu B2B i LP zawodu. Jeden na stronę, dla nagłówka, który niesie zawód i zakres.
heading-lTytuł strony albo artykułu. Jeden na stronę.
heading-m, heading-s, heading-xsHierarchia sekcji, po kolei, bez przeskoków. Karta decyzji: nazwa produktu na heading-s, nagłówki kolumn porównania na heading-xs.
body-lLead artykułu i LP. Esencja wyciągu dla zarządu, czyli to, co czyta osoba, która nie otworzy rozwinięcia.
body-mDomyślny tekst treści i interfejsu. Rozwinięcie wyciągu, uzasadnienie propozycji w karcie decyzji.
body-sTekst pomocniczy: opisy pól, przypisy, wskazanie dokumentu źródłowego przy twierdzeniu o produkcie.
label-l, label, label-sEtykiety pól, przycisków i kontrolek; label-l w dużym przycisku. Waga 500 odróżnia je od body-* przy tym samym rozmiarze.
captionMetadane, znaczniki czasu, mikrocopy statusu, etykiety plakietek.

Teksty, które wchodzą w te poziomy, mają własne strony: mikrocopy dla stanów i etykiet, komunikaty dla nagłówków i claimu.

KlientPrzedsiębiorcaPracodawcaKlient indywidualny
Gdzie czytaLP zawodu i ekran sprawyprezentacja i dokument dla zarządu, materiały dla HRkalkulator i ścieżka zakupu
Co dominujebody-m w gęstym rytmie, heading-s i heading-xs porządkują ofertębody-l w esencji, body-m w rozwinięciulabel i body-s przy polach, caption przy zastrzeżeniach
Czego pilnujemyróżnica między label a body-m pokazuje, co jest etykietą, a co wartościąesencja i rozwinięcie mają różny poziom, nie ten sam rozmiar pogrubionycena i warunek jej ważności nie schodzą do caption
  • Nie dodajemy poziomu dla pojedynczego napisu. Trzynaście poziomów to cała skala.
  • Nie zmieniamy rozmiaru, interlinii ani wagi osobno w miejscu użycia. Wyróżnienie w zdaniu to <strong>, nie klasa wagi.
  • Krój monospace zostaje przy kodzie i wartościach technicznych. Nie jest krojem ozdobnym.
  • Próg desktopu niesie token breakpoint.md. Cztery największe poziomy przełącza @variant md, więc zmiana progu to jedna wartość: siatka i responsywność.
  1. Poziom renderuje się klasą text-<rola>, a nie zestawem własnych deklaracji. Sprawdzasz to wyszukaniem font-size i font-weight w kodzie poza tokenami.
  2. Cztery największe poziomy zmieniają rozmiar na progu 48rem. Oglądasz tę samą stronę poniżej i powyżej progu.
  3. Długi nagłówek z polskimi znakami łamie się bez przycięcia i bez utraty diakrytyków, w jasnym i ciemnym trybie.
  4. pnpm design:check przechodzi, łącznie z kontrolą literałów w zbudowanym styleguide.
  5. pnpm design:audit przechodzi baterię krojów (audit/fonts.spec.ts): każda serwowana twarz Geist Sans i Geist Mono wczytuje się z polskimi znakami, kod stoi na Geist Mono, a tekst ma wyłącznie serwowane wagi.

Tokeny i zależności, Siatka i responsywność, Dokumenty, oferta i prezentacja, Standard dostępności, skala w prymitywach — źródło w repo: packages/design/tokens/src/core.tokens.json, generator klas skali — źródło w repo: packages/design/tokens/src/build.ts, dostawa krojów — źródło w repo: packages/design/ui/src/styles/fonts.css.