Forest is a library that provides a Clojure DSL for CSS, similar to Garden but with more limited expressivity and a stronger focus on modularity.
Add the following dependency to your project.clj
or build.boot
:
The library is intended to be used through the defstylesheet
macro, which at load-time injects the compiled stylesheet into the DOM of the browser. Additionally, the macro exposes all CSS classes and IDs as Clojure variables with the real name of the class or ID. By using these variables instead of string literals, each stylesheet becomes independent and won't conflict with anything else.
(ns sample
(:require [forest.macros :refer-macros [defstylesheet]]))
(defstylesheet my-stylesheet
[.my-class {:font-weight "bold"}])
my-class ;; => "<unique class name>"
;; This can be used with e.g. Om
(dom/div {:className my-class} "Bold text")
;; Or with something like Sablono
(html [:div {:class my-class} "Bold text"])
Like the @extend
keyword from Sass, classes can extend each other. Use the :composes
keyword in the style declaration:
(defstylesheet my-stylesheet
[.base {:padding-bottom "16px"}]
[.heading {:composes base
:font-weight "bold"}]
[.copy {:composes base
:font-size "16px"}])
However, unlike Sass, this does not duplicate selectors in the stylesheet, but rather modifies the exposed class name to include all base classes. Multiple classes can be extended by specifying a vector or list of classes.
Since composition works on class name identifiers, you can use Clojure's built-in features for building modules:
;; base.cljs
(ns base
(:require [forest.macros :refer-macros [defstylesheet]]))
(defstylesheet base-styles
[.base {:padding-bottom "16px"}])
;; typography.cljs
(ns typography
(:require [base :as base]
[forest.macros :refer-macros [defstylesheet]]))
(defstylesheet typography-styles
[.heading {:composes base/base
:font-weight "bold"}]
[.copy {:composes base/base
:font-size "16px"}])
Forest has support for descendant and immediate child selectors. When using these, the :composes
feature becomes unavailable, since it's not well-define how these features interact. This limitation is inherited from the CSS Modules spec, but might be lifted in the future.
Descendant selectors are used like follows:
(defstylesheet styles
[(descendant .container .element)
{:padding-bottom "16px"}]
;; Translated into the selector ".container .element"
[(> .parent .immediate-child)
{:font-size "16px"}]
;; Translated into the selector ".container > .element"
)
The choice of the rather verbose word descendant
is intentional -while the arbitrary descendant selector is a powerful concept, it's often not suitable in larger CSS codebases. They can in most cases be replaced by immediate child selectors, which don't suffer from the same scalability problems.
There's also a class-names
function that can be useful for combining classes when building interfaces:
(ns sample
(:require [forest.macros :refer-macros [defstylesheet]]
[forest.class-names :refer [class-names]]))
(defstylesheet my-stylesheet
[.list-item {:list-style "square"}]
[.is-selected {:font-weight "italic"}])
(html
[:ul
(map (fn [item]
[:li {:class (class-names list-item
{is-selected (selected? item)})}])
items)])
class-names
joins together all truthy (non-nil/non-false) arguments, flattens arrays and maps, and only picks maps with truthy values.
To work on Forest, you'll need Boot installed somewhere on $PATH
. To iterate on the unit and integration tests, run:
boot watch test-all
This runs all Clojure and ClojureScript tests. The main defstylesheet
macro and related compiler code is written in Clojure. The ClojureScript tests run some integration tests and test the class-names
function.
If you want to attach a REPL to the running tests, e.g. through Cider, run:
boot repl watch test-all