Health Samurai Official Logo
Статьи
/
MacroCSS (Clojure). Next step of atomic CSS
//

MacroCSS (Clojure). Next step of atomic CSS

Introduction

How should we organize code of our user interfaces? The community of web developers divides into several camps. Some state that we should have components hierarchy, where each component has a purpose – e.g. button, avatar or link, components may be a set of other components – e.g. menu (set of buttons), table (set of columns and rows) etc. Some state that we should have set of tiny elements, each of the element describes a single state of element styling, it is called atomic approach.

Component based approach gives an opportunity of fast start with a pack of well-designed components, whereas atomic approach key feature is freedom to compose your own components at a cost of some amount of work.

Atomic CSS

Brad Frost considers that atomic elements combine together to form molecules. These molecules can combine further to form relatively complex organisms. To expound a bit further:

  • Atoms are the basic building blocks of all matter. Each chemical element has distinct properties, and they can’t be broken down further without losing their meaning.
  • Molecules  are groups of two or more atoms held together by chemical bonds. These combinations of atoms take on their own unique properties, and become more tangible and operational than atoms.
  • Organisms  are assemblies of molecules functioning together as a unit. These relatively complex structures can range from single-celled organisms all the way up to incredibly sophisticated organisms like human beings.

Let's take a look, what we consider atomic when we speak about atomic CSS, so here is Tailwind CSS documentation:

We see that one class name describes just a single set of properties. If we need to style our component – we just combine classes:

<figure class = “md:flex bg-gray-100 rounded-xl p-8 md:p-0”>
<img class = “w-32 h-32 md:rounded-none rounded-full mx-auto”>

Combination of classes describes the component. So the atomic way does not contradict the component-based approach, but it significantly eases the pain of working with CSS:

  • you do not have to carry any cognitive load to think about naming of classes, you just combine style properties and observe the result, you may give some semantically based element name (e.g. “nav”, “figure” etc.) - though it is not obligatory;
  • you do not actually work with editing the CSS file – CSS preprocessors swipe out unnecessary CSS (however in some frameworks like Tachyon and Bootstrap you will have to use third party libraries like Purge CSS)
  • you do not face with the specificity issues, redundant or bloated code, code maintenance becomes easier – you just style the particular component properties and it works

Although, being a rather usable product Tailwind CSS (as well as Tachyon and Bootstrap) – it has some peculiarities (not to say flaws). What are they? Main disadvantages are the following:

  • sometimes syntax becomes too heavy, especially when you have to define properties of pseudoclasses – you can not just declare it once, but have to repeat it every time you define style of pseudoclass. E.g. if we need to define background, text size and other features of hovered element – we need to explicitly state it:
<div class “hover:bg-blue-300 hover:underline hover:text-blue-700”>

Would it be more convenient if we could combine pseudoclasses in groups?

  • As you may have already noticed – we are limited with the values that are given us by library maintainers. If you want to have custom values – there is a way to define it, but it takes some effort and the workflow becomes similar to usual CSS styling: you define the class values in one place and then use the in another. The complexity returns as we need some customization:
//tailwind.config.js
module.exports = {
theme: {colors: //
 // Configure your color palette here
  }
 }
}

MacroCSS

MacroCSS is a library developed by HealthSamurai team. It was inspired by Tailwind CSS and the idea was to avoid the problems of its predecessor:

  • custom values and classes should be easily passed
  • no unnecessary heaviness in style declaration
  • hot-reload is available

Brief look

It is written in Clojure – functional language, JVM hosted LISP. Clojure is homoiconic language, which means code itself is a datastructure of language, it may be threated as ordinary data. Let's take a look at how it works.

Clojure code:

(defn h1 
[& content]
  [:h1 {:class (c
                :text-6xl
                [:m 10]
                [:rounded :full]
                [:border 5]
                [:px 5]
                [:bg :green-300]
                :font-extrabold
                [:text :gray-900]
                :tracking-tight
                [:smartphone :text-xl])
:key (gen-key)} content])

Generated CSS file:

.components_hiccup-109-16 {
  --text-opacity: 1;
  border-width: 5px;
  --bg-opacity: 1;
  margin: 2.5rem;
  font-weight: 800;
  font-size: 4rem;
  background-color: rgba(154,230,180,var(--bg-opacity));
  padding-right: 1.25rem;
  letter-spacing: -0.025em;
  border-radius: 9999px;
  color: rgba(26,32,44,var(--text-opacity));
  padding-left: 1.25rem;
}

@media (max-width: 415px) {
 .components_hiccup-109-16 {
 font-size: 1.25rem; }
}

So, things you only need to know are the following:

  • predefined style declarations are just keywords -> (c :text-3xl)
  • style declarations that take arbitrary values are vectors like -> (c [:m 10])
  • pseudoclasses styles are easily combined -> (c [:hover :underline [:bg :green-500]]])
  • media queries have the same syntax as pseudoclasses -> (c [:smartphone [:w 300]])

As we see – it is fully atomic. You just combine declarations of style after macro call. Some of them have predefined values. Some may take arbitrary values. If you are already familiar with Tailwind CSS – MacroCSS has similar structure of intuitive classnames.

Documentation for MacroCSS is organized the same way as in TailwindCSS:

Customize

If you want to have your own classnames or redefine value for existing classname – there are instruments to perform it.

To define a single class – you need to declare a new method for multimethod:

(defmethod rule :rounded-t
  ([_] [[:& (rounded nil :border-top-left-radius :border-top-right-radius)]])
  ([_ x] [[:& (rounded x :border-top-left-radius :border-top-right-radius)]]))

To define a bunch of classes – use 'defrules' function, it actually just declares new methods for you:

(def background-attachment {:bg-fixed  {:background-attachment "fixed"}
                                        :bg-local    {:background-attachment "local"}
                                        :bg-scroll    {:background-attachment "scroll"}})

(defrules background-attachment)

After declaring background attachments styles – you may use them in your code:

(c :bg-fixed)

If you want your all or some classes to take parameters – it is also possible. When you declare the value of styles – use lambda-functions for classes that take arguments. Let's write the same bunch of classes with different approach:

(def background-attachement
 {:bga
 (fn [x] {:background-attachment (when (#{:fixed :local :scroll} x)
                                                            (name x)})})

 (defrules background-attachment)

So – now we can use this class another way:

(c [:bga :fixed])

Media rules

Media rules have more complicated mechanism of declaring. We will explain the reason for it further. Default media rules values are:

{:screen {:screen true}
                :smartphone {:max-width "415px"}
                :ereader {:max-width "481px"}
                :p-tablets {:max-width "768px"}
                :l-tablets {:max-width "1025px"}
                :desktop {:min-width "1200px"}}

If you want to set your own media rules – use set-own-mediarules! function, it takes a map that will be new set of media rules.

(set-own-mediarules! {:sm {:min-width "415px"}
                                     :tb  {:min-width "768px"}
                                     :desktop {:min-width "1200px"}}})

If you want to add some media rules – use extend-media-rules! function:

(extend-media-rules! {:samsung-note {:max-width “430px”}})

How it works

Why do we use macro and not just a function? The answer is not that trivial. If we look at the following code – we will understand that a style should already be computed before the code of components is evaluated:

(defn some-component
  []
  [:div {:class (c :text-3xl)}])

It is just impossible to perform without using a macro.

How do macros work? C. Emerick describes it the following way - it is between the read and evaluation steps where compilation happens and macros occupy a privileged status compared to functions. Whereas function calls in source code carry through to the compiled representation of that code where arguments are evaluated and passed to the function as parameters yielding some result at runtime, macros are called by the compiler with their unevaluated data structures as arguments and must return a Clojure data structure that can itself be evaluated.

There are two versions of c macro: plain c and c-eco macro. What is the difference between them?

(defmacro c
  [& rules]
  (c-fn &env rules))

(defmacro c-eco
  "Uses only hashed version of classname. Is recomended for release purposes, because it minimizes resulting CSS file."
  [& rules]
  (c-fn nil rules))

As we see – c macro takes and additional argument &env. What is &env? &env – binding for all local bindings, it work slightly different in Clojure and ClojureScript.

In MacroCSS we use it to get namespace, line an column in code where macro was called:

.components_hiccup-255-18 {
  margin-left: auto;
  color: rgba(113,128,150,var(--text-opacity));
  --text-opacity: 1;
  transition-duration: 200ms;
}

It makes development more convenient because you always know which component to debug and it makes hot-reload of styles more reliable.

c-eco macro is recommended for release purposes – it takes all arguments passed to c macro and calculates a hash of it:

(c-eco [:pt 8])
;; class :c-1581282564

That means that same set of properties will almost always return the same hash, thus we reduce the number of classes used in CSS file, especially when you use reusable components.

We take the following steps to perform style calculations:

  • we have a call of a macro with style declaration -> (c [:p 8] [:smartphone [:p 15]])
  • style declaration expands to {:padding “2rem”} and stores that value in associative map atom where key is a hash or place of macro-call {:ns_core-115-5 {:padding “2rem”}}
  • after we know the class-name – we compute media queries, they are stored in separate atom
  • all styles are compiled using GardenCSS mechanisms, so we can actually use not only hiccup-like declarations:
{:font-weight "300"}

but also garden-like declarations:

[:& {:color "red"}] [:&:target {:color "green"}]
[[:.some-arbitrary-class {:bg :blue-400}]]

  • after all styles are compiled – they are spit into stylo.css file

Links

Author

Artem Alexeev, full-stack developer at Health Samurai LLC

Подписывайся на еженедельную рассылку!

Наши инженеры регулярно делятся профессиональным опытом в формате статей, live-coding, видео, рекомендаций и т.д.

Открытые вакансии

Junior Clojure Developer

Если у тебя 2+ года опыта в web-разработке, ты хочешь писать на Clojure – присоединяйся к нам.

Full-stack JS Developer

Если у тебя 3+ года опыта в JS, являешься или хочешь развиваться в сторону full-stack инженера – присоединяйся к нам.

Product Owner / Business Analyst

Если умеешь "распутывать" бизнес-контекст, любишь общаться и организовывать разработку, проектировать UI/UX.

Health Samurai Company Logo