ASCSS
Nie lubię koncepcji kryjących się za Atomic CSS i Tailwindem, ale swego czasu powiedziałem, że ASCSS byłoby o wiele lepsze niż ACSS. Nie tak dawno powtórzyłem to przekonanie. Przyszła zatem pora, by wcielić je w życie.
Atomic CSS vs Atomic SCSS
Wspomniany już artykuł na Na Frontendzie dobrze wyjasnia, czym jest ACSS. W skrócie: style konstruujemy przez nadawanie elementom atomowych klas. Atomowa klasa z kolei to po prostu klasa określająca jeden (i tylko jeden) styl, np. .p-10
ustawi padding
na 10px
. Pożyczając słownictwo z obszaru design systems, można stwierdzić, że takie klasy są odpowiednikami design tokens. A to oznacza, że żeby stworzyć jakkolwiek wyglądający komponent, trzeba mu nadać sporo klas. Przykład wprost ze strony Tailwinda:
<button class="bg-blue-500 hover:bg-blue-600 text-white font-bold py-2 px-4 rounded">
Button
</button>
Bardzo szybko tego typu kod staje się mało czytelny, a brak podziału na warstwy prędzej czy później nas ugryzie (np. zmienił się wygląd komponentu używanego w kilku miejsach, a jeszcze nie było czasu, by stworzyć choćby zalążek biblioteki komponentów).
Porównajmy to z innym przykładem:
<button class="button button_cta">
Button
</button>
.button_cta {
.bg-blue-500;
.hover:bg-blue-600;
.text-white;
.font-bold;
.py-2;
.px-4;
.rounded;
}
Tutaj HTML jest czysty, natomiast wszystkie atomowe klasy są aplikowane w preprocesorze CSS. Dzięki temu wciąż mamy zachowany rozdział warstw, a style wciąż możemy komponować przy użyciu prostych, atomowych klas. I, co najważniejsze, mamy pewność, że po zmianie pliku ze stylami dany komponent będzie wyglądał tak samo w każdym miejscu strony.
Tak naprawdę ACSS przeniesione na poziom preprocesora to zbiór krótkich aliasów dla tradycyjnych stylów (np. .py-2
to alias dla padding-left: 2px;padding-right: 2px;
). I to w wielu wypadkach jest całkowicie wystarczające.
Implementacja
Skoro pomysł chodzi mi już po głowie od pewnego czasu, to stwierdziłem, że fajnie byłoby go w końcu choć częściowo zrealizować. Co prawda istnieje @apply
w Tailwindzie, ale nie do końca odpowiada mi jego podejście, w którym możemy posługiwać się wyłącznie uprzednio stworzonymi klasami. Atomizer pozwala na o wiele większą swobodę (np. .P(23)
ustawi padding
na 23px
) i dlatego postanowiłem wykorzystać jego składnię.
Wybór preprocesora
Na samym początku musiałem wybrać preprocesor, w którym chciałem zaimplementować swoje narzędzie. Tutaj wybór był dość prosty i – mimo że zawsze nazywałem swój pomysł ASCSS – szybko zdecydowałem się na PostCSS. To parser CSS-a napisany w JS-ie, z niezwykle prostym API dla pluginów. Dzięki temu byłem w stanie stworzyć działającą wersję w ciągu kilkunastu minut.
Składnia
Niestety, PostCSS ma też swoje ograniczenia, a największym z nich jest niezwykle utrudnione rozszerzanie wbudowanego parsera. Tym sposobem nie byłem w stanie jeden do jednego przenieść składni wykorzystywanej przez Atomizera.
Na szczęście odkryłem, że PostCSS dość liberalnie podchodzi do tzw. at-rules (reguł poprzedzonych @
, jak np. @media
), więc ostatecznie udało mi się zaimplementować niemal identyczną składnię:
div {
@P( 10px );
}
Pytanie jednak, czy aby na pewno taka składnia jest nam potrzebna, skoro CSS ma już bardzo dobrą składnię dla par klucz–wartość. Dlatego też dodałem alternatywną składnię:
div {
p: 10px;
}
Alternatywna składnia zdecydowanie bardziej mi się podoba i jest czytelniejsza, ale – może sprawiać problemy, gdy będę próbował dodać bardziej zaawansowane rzeczy z Atomizera (jak style dla :hover
). Na chwilę obecną obydwie składnie działają tak samo, bo jedyne, co zaimplementowałem, to atomowe klasy.
Kod
Kod jest dostępny na GitHubie. Najwięcej miejsca w nim zajmuje… Map
zawierająca wszystkie obsługiwane aliasy. Natomiast najmniej – sam plugin do PostCSS-a. Ten zawiera raptem dwie metody: Declaration
oraz AtRule
, zajmujące się odpowiednio składnią CSS-ową (p: 10px
) oraz atomizerową (@P( 10px )
).
Plugin można też pobrać z npm-a.
Jeśli ktoś się zastanawia, skąd taka dziwna nazwa pluginu: bardzo często moje projekty są przerobieniem po mojemu innych narzędzi czy pomysłów. Dlatego też najczęściej w takim wypadku biorę oryginalną nazwę projektu i zapisuję ją od tyłu. Tym samym atomizer
przemienił się w rezimota
.
Co dalej?
Jak na razie dodałem jedynie obsługę dla atomowych klas – a i tak nie wszystkich. Ominąłem klasy ustawiające konkretne wartości filter
(np. Blur( value )
, które jest tłumaczone na filter: blur( value )
). Ominąłem też wszystkie klasy, których nazwy zawierały start
i end
, zamieniając je na klasy zawierające l
i r
. Wynika to z tego, że w Atomizerze start
i end
są używane zamiast wartości left
i right
. A to w dobie powstających własności logicznych jest mocno dyskusyjnym rozwiązaniem. Jeśli będę dodawał wsparcie dla klas ze start
i end
, będą to właśnie własności logiczne.
Wypada dodać też wsparcie dla klas pomocniczych i wspomnianej składni dla pseudoklas. Są też specjalne wartości dla klas atomowych… Ogólnie ścieżek rozwoju jest dużo – i to dopiero żeby dobić do obecnych możliwości Atomizera. A przecież można jeszcze dodać choćby możliwość wykorzystywania wartości dostarczanych bezpośrednio z design tokenów!
Wnioski
PostCSS jest niezwykle prosty i przyjemny w obsłudze. Szkoda, że parsery JS-a takie nie są. I szkoda, że nie mają takiej dokumentacji jak PostCSS…
A jeśli chodzi o samo rozwiązanie: cóż, nie wygląda to tak, jakbym się spodziewał. Jest zdecydowanie bardziej nieczytelne, niż zakładałem (konia z rzędem temu, kto bez wcześniejszego kontaktu z Atomizerem wie, jaki styl ustawia klasa .Bdrstl
). Jeśli działa to sensownie dla podstawowych własności (.P
– padding
, .W
– width
itd.), tak próba określania w taki sposób np. transition-timing-function
(.Trstf
) zaczyna wyglądać groteskowo. Prawdę mówiąc być może wygenerowanie atomowych klas na podstawie wartości design tokenów byłoby lepszym wyjściem. Wówczas dostalibyśmy jasno okreslony zbiór możliwych do wykorzystania klas i można byłoby je dowolnie komponować (czyli @apply
, które na wstępie odrzuciłem…). Tylko że to już zupełnie inne narzędzie, choć wciąż w duchu A(S)CSS.
Komentarze
Przejdź do komentarzy bezpośrednio na Githubie.
Dawne komentarze
Ten blog wcześniej korzystał z systemu komentarzy Disqus. Jednakże pożegnaliśmy się i postanowiłem, że zaimportuję do nowej wersji stare komentarze z niego. Cóż, jego system eksportu na wiele nie pozwala…
Nie podoba mi się koncept tailwinda w ogóle. Taki sposób budowania klas i stylów przypomina mi nadmierne korzystanie z mixinów i tym podobnych narzędzi. Być może na początku to fajnie działa, ale utrzymanie takiego projektu długoterminowo się nie sprawdzi.
Od czasu powstania tego wpisu minęło już kilka miesięcy, a Tailwind mocno się rozwinął. Ciekawy jestem czy i Twoja opinia uległa modyfikacji. Z jednej strony rozumiem developerów, którzy lubią Tailwinda: szybkość budowania UI, spójność UI itp. Z drugiej strony nie rozumiem w czym pisanie tego samego w CSS z pomocą PostCSS jest wolniejsze czy mniej spójne. Mam trochę wrażenie, że Tailwind napędzają developerzy Reacta, którzy chyba w ogólności nie za bardzo lubią CSS, bo mają z nim problem organizacyjny (Styled Components, CSS modules w osobnych plikach itd.) Poza tym praca w React sprzyja zacieraniu granic między HTML, CSS i JS. Piszę to z własnego doświadczenia, bo od pięciu lat sam pracuję zawodowo w React. Ale jestem krytyczny, po pracy wolę Vue czy ostatnio Svelte, które moim zdaniem kierują wyobraźnię developera we właściwszym kierunku niż React. Mam na myśli, że komponenty w Svelte czy Vue rozdzielają HTML, CSS i JS oraz sugerują, że najważniejszy jest HTML, CSS jest dodatkiem a JS przyprawą, z którą trzeba ostrożnie.
Tailwind się rozwinął, ale po wejściu na jego stronę wciąż widzę ten sam tekst:
Nie miałbym problemu z Tailwindem, gdyby reklamował się jako rozwiązanie konkretnych problemów. Niemniej kreuje się na wielką rewolucję – a tym najzwyczajniej w świecie nie jest.
Moja opinia nie zmieniła się za bardzo, dalej uważam, że tego typu rozwiązanie ma większy sens jako preprocesor. Albo jako warstwa pośrednicząca między design systemem a implementacją. Narzędzie, które automatycznie generuje mi zmienne CSS na podstawie tego, co tworzę w Figmie albo innym narzędziu, brzmi jak coś, czego bym z przyjemnością używał.