Skip to content
Michael A. Wright edited this page Oct 27, 2017 · 21 revisions

CSS in CLJ(S)

Combining your CSS with your HTML elements, using the power of CLJ(S) to produce clean and well organized CSS. Putting an end to this. No inline styles, just CSS (and all of it).

See: css-in-js for all sorts of JS implementations and some other thoughts on the subject. This video provide an excellent summary of "why". Their solution differs in what shadow.markup does, but the "why" is the same.

Example

(ns my.demo
  (:require [shadow.markup.css :as css :refer (defstyled)]))

(defstyled page-title :h1
 [env]
 {:color "red"
  "&:hover"
  {:color "green"}})

We now have an element named page-title which will be a h1 HTML element. Using this element as a function will produce a ReactElement in CLJS and a string in CLJ.

(page-title {} "hello world")

In essence this produces

<h1 class="my-demo--page-title">hello world</h1>

and this CSS:

h1.my-demo--page-title {
  color: red;
}
h1.my-demo--page-title:hover {
  color: green;
}

ClojureScript + React

In ClojureScript this actually produces a ReactElement, so you should be able to use it with React directly or any wrapper that understands ReactElement (ie. om, om.next). The CSS is automatically created and injected when you use the element the first time. No more setup required.

(js/ReactDOM.render (page-title {} "Hello world") ...)

You can also use these elements with the reagent and its hiccup-like syntax:

[:div.boring-css
  [page-title "Hello World"]]

If you want plain React elements without CSS rules the shadow.markup.react namespace provides functions for each React HTML element:

(require '[shadow.markup.react :as html])
(html/div
  (html/h1 "hello world")
  (html/p "foo."))

This is very similar to what om.dom and other similar libraries provide. I recommend using only defstyled though since this way you can give a semantic name to every piece of your HTML. (ie. page-title instead of generic h1). You can always add styles later, without styles it is just a generic h1.

Clojure

In Clojure the elements just produce strings of HTML.

CSS generation on the server is currently a manual process, as you need to tell the generator which styles you want. You can call (css/generate-css-for-elements css-env [my.demo/page-title]) to produce the CSS for just this element.

There is a helper to allow quick collection of all elements defined in a namespace:

;; run this in a REPL or a script.
(require '[shadow.markup.css :as css])
(require 'my.demo)

(def css-env {}) ;; see below

(->> '[my.demo]
     (css/find-elements)
     (css/generate-css-for-elements css-env))

This only produces a string, how you get that into your page is left for you do decide. You can write it into a file and load that, or inline the result into your generated HTML. Once I have settled on a final API this will probably be easier to automate.

CSS Maps

The CSS map must follow some simple rules. Allowed keys are:

  • Keywords: CSS properties
  • Strings: Selectors
  • other defstyled elements for nested selectors

CSS properties

Keywords should map to CSS values, most of them should be strings. Numbers are treated as pixels ({:font-size 14} => "font-size: 14px;"), vectors are treated as collection of values ({:padding [0 10]} => "padding: 0 10px;").

CSS selectors

Strings are treated as nested selectors and their value must be another CSS map. They must contain an & to place the original selector.

(defstyled h1 :h1
 [env]
 {:color "red"
  "&.center"
  {:text-align "center"}})

(h1 {:class "center"} "hello world")

;; => <h1 class="user--h1 center">hello world</h1>

(css/generate-css-for-elements {} [h1]) now produces and additional rule. This rule is intentionally specific to avoid conflicts with other elements. The "engine" will replace the & with h1.user--h1, so placement of the & is important but should allow almost any combination of CSS selector I could come up with so far. (FIXME: mention % and referring other elements).

h1.user--h1 {
  color: red;
}

h1.user--h1.center {
  text-align: center;
}

CSS environment

Each defstyled is passed an env variable. This usually is a Clojure map which you can use to store variables you want to use in your CSS (ie. a standard background color). Since the style generation is just plain Clojure you can integrate this easily. In CLJS you can configure the env using the shadow.markup.css/set-env! fn. It must be called before the first element is used (not defined). If your app has a start fn, call it there. Another strategy can be to have a my.app.theme namespace and have every namespace that includes defstyled depend on it. This way it is ensured that your CSS environment is setup before you use any elements. The env can contain anything you like and provide a simple way to provide theme support for re-usable components.

(ns my.app.theme
  (:require [shadow.markup.css :as css]))

(css/set-env!
  {:colors
   {:bg "#eee"
    :border "red"}
   :media
   {:smartphone
    "@media (max-width: 400px)"}})

(ns my.app.html
  (:require [shadow.markup.css :as css :refer (defstyled)]
            [my.app.theme]))

(defstyled page-title :h1
  [env]
  {:padding 10
   :font-size 24
   :background-color (-> env :colors :bg)
   :border (str "1px solid " (-> env colors :border))
   (-> css :media :smartphone)
   {:font-size 16}})

CSS classnames

CSS classnames are generated by using the current ns and the name of the defstyled. You can override that using {:shadow.markup.css/alias "my-alias"} in the ns or def metadata.

(ns my.demo
  {:shadow.markup.css/alias "foo"}
  (:require [shadow.markup.css :as css :refer (defstyled)]))

(defstyled ^{:shadow.markup.css/alias "bar"} page-title :h1 [env]
 {:color "red"})

;; would yield h1.foo--bar, I only do this for the `ns` usually.

Internals

defstyled is a rather simple macro, all it does is this:

(def h1 (css/element* "h1" "my-demo--page-title" (fn [env] {:color "red"}))

which creates a StyledElement with el-type="h1" and el-selector="user--h1" and a style-fn. the user--h1 is generated based on the ns you are in (defaults to user in case of a CLJ REPL) and the name of the def. The style-fn is given a CSS environment (explained later) and is expected to produce a normal map with certain rules. No macro is used here, it is all plain Clojure. You can use merge, call functions, etc.