Architektura komponentów
Zasady mieszkają w komponentach
Zasady mówią, że interfejs ma stać w miejscu. Architektura decyduje, czy to się uda: jeśli o rezerwie na błąd i wymiarach skeletonu musi pamiętać każdy widok osobno, przegrasz przy trzecim sprincie. Jeśli są wbudowane w komponent, dobre nawyki przychodzą same. Ta strona porządkuje, jak dzielić interfejs na warstwy, jak nazywać ich elementy i jak projektować API komponentu, żeby liquid design był właściwością systemu, a nie dyscypliną jednostek.
Trzy warstwy zamiast pięciu atomów
Atomic design Brada Frosta nauczył branżę składania interfejsu z małych części. Bierzemy z niego to, co działa, i zostawiamy to, co w praktyce produkuje dyskusje zamiast komponentów.
Bierzemy
- Hierarchię składania: małe rzeczy budują większe, nigdy odwrotnie.
- Wspólny język zespołu: designer i programista mówią o tym samym poziomie abstrakcji.
- Inwentarz interfejsu: zanim narysujesz widok, wiesz, z czego on powstanie.
Zostawiamy
- Pięć poziomów: granica między molekułą a organizmem to najczęstsza jałowa dyskusja w historii design systemów.
- Kategoryzację dla samej kategoryzacji: przenoszenie komponentu między folderami nie poprawia produktu.
- Metaforę chemiczną w nazwach folderów: atoms/ mówi mniej niż ui/.
Zamiast atomów, molekuł i organizmów wystarczą trzy warstwy z jedną regułą: zależności płyną wyłącznie w dół. Tak jest zbudowana także ta platforma.
ui/
Prymitywy
Pojedyncze elementy interfejsu: przycisk, pole, badge, skeleton. Nie znają domeny, nie wiedzą, w jakim widoku stoją, a wszystkie swoje stany mają wbudowane. Zależą wyłącznie od tokenów.
components/
Kompozycje
Powtarzalne bloki produktu sklejone z prymitywów: szkielet lekcji, blok kodu, ramka demo. Znają konwencje platformy, ale nadal nie znają konkretnej treści. Zależą od prymitywów i tokenów.
app/
Widoki
Strony i layouty. Układają kompozycje w siatki, ustalają odstępy i wstawiają treść. Niczego nie stylują od nowa: jeśli widok potrzebuje nowego wyglądu, brakuje wariantu piętro niżej.
BEM to sposób myślenia, nie składnia
BEM rozwiązywał problem globalnego CSS: skoro wszystko widzi wszystko, scope trzeba było utrzymać konwencją nazw. W świecie komponentów scope daje narzędzie, ale rozbiór interfejsu na blok, element i modyfikator pozostaje aktualny. Zmienia się tylko miejsce, w którym te pojęcia mieszkają.
Blok
.badge
Komponent
<Badge>
Element
.badge__icon
Markup wewnątrz komponentu
span w JSX
Modyfikator
.badge--danger
Prop wariantu
tone="danger"
Scope przez konwencję nazw
dyscyplina zespołu
Scope przez granicę komponentu
gwarantuje narzędzie
.badge {
display: inline-flex;
border-radius: 999px;
padding: 0.25rem 0.75rem;
}
.badge__icon { width: 1em; }
.badge--danger {
border-color: var(--color-danger-400);
color: var(--color-danger-300);
}
<span class="badge badge--danger">
<svg class="badge__icon">…</svg>
Błąd zapisu
</span>const tones = {
neutral: "border-ink-600 bg-ink-800 text-mist-300",
danger: "border-danger-400/30 bg-danger-400/10 text-danger-300",
} as const;
export type BadgeTone = keyof typeof tones;
export function Badge({ tone = "neutral", children }) {
return (
<span className={`inline-flex rounded-full border px-3 py-1 ${tones[tone]}`}>
{children}
</span>
);
}Klasyczny BEM nadal wygrywa tam, gdzie nie ma bundlera ani komponentów: projekt w czystym HTML i CSS, szablony CMS, strony bez kroku budowania. Wszędzie tam to wciąż najlepsza dostępna dyscyplina.
Kontrakt komponentu
API komponentu to obietnica składana każdemu widokowi, który go użyje. Cztery reguły utrzymują tę obietnicę w mocy niezależnie od tego, kto i kiedy komponent wywoła.
Wygląd z zamkniętej listy
Warianty to enumeracja: tone, variant, size jako klucze mapy, nie wolne stringi. Typ podpowiada dostępne opcje, a nowy wygląd wymaga świadomej decyzji w jednym miejscu zamiast doraźnej klasy w widoku.
Stan to boolean, wygląd to wariant
loading, disabled i error opisują, co się dzieje. primary i danger opisują, czym komponent jest. Mieszanie tych osi kończy się propsami w rodzaju isRedButton, których nikt nie umie użyć poprawnie.
Kompozycja zamiast konfiguracji
Treść wchodzi przez children, nie przez rosnącą listę propsów w rodzaju iconLeft, subtitle i footerText. Komponent z dziesięcioma propsami konfiguracyjnymi to zwykle trzy komponenty sklejone na siłę.
Zero marginesów zewnętrznych
Komponent kończy się na własnej krawędzi. Odstępy między elementami należą do rodzica i jego gap, bo tylko rodzic zna kontekst. Badge z wbudowanym margin-bottom psuje każdy layout poza tym jednym, dla którego powstał.
<button
aria-busy={loading}
className="inline-grid min-h-11 place-items-center rounded-full px-6"
>
<span className={`col-start-1 row-start-1 ${loading ? "invisible" : ""}`}>
{children}
</span>
<span
aria-hidden
className={`col-start-1 row-start-1 ${loading ? "" : "invisible"}`}
>
<Spinner />
</span>
</button><section className="flex flex-col gap-4">
<Badge tone="ok">Gotowe</Badge>
<Button>Zapisz zmiany</Button>
</section>
.badge { margin-bottom: 16px; }Reguły architektury
- Zależności płyną w dół: ui/ nie importuje z components/, components/ nie importuje z app/. Import w drugą stronę to sygnał, że komponent stoi na złej warstwie.
- Zasady platformy mieszkają w komponentach, nie w code review: miejsce na błąd, stałe wymiary stanu ładowania i cele dotyku są wbudowane w prymityw, więc widok dostaje je za darmo.
- Każdy stan komponentu ma te same wymiary: treść, ładowanie i błąd dzielą jeden szkielet, bo przełączenie stanu nie ma prawa ruszyć layoutu.
- Komponent nie zna swojego kontekstu: zero marginesów zewnętrznych, zero założeń o szerokości rodzica, rozmiar dyktuje kontener.
- Abstrakcja przy trzecim użyciu: skopiować kod dwa razy jest tańsze niż utrzymywać zły komponent współdzielony, którego API zgadywało przyszłość.
- Jedna nazwa wszędzie: komponent nazywa się tak samo w kodzie, w Figmie i w rozmowie zespołu, bo tłumaczenie nazw to podatek płacony przy każdym przekazaniu.