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 (.Ppadding, .Wwidth 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.