My Garden CSS has ascended
Table of Contents
I’ve been continually seeking the answer to Garden CSS’s title question: “what’s possible when you trade a preprocessor for a programming language?” I have used Garden exclusively for years, happily wielding garden-gnome to implement hot-loading on my front-end for an excellent development experience. Writing CSS with Clojure data-structures is a hands-down win over raw css-writing, and Garden also gives useful shortcuts that some of the preprocessors also have like lighten
, darken
, &
selectors, and more. Still, though, I felt something missing: things were beautifully data-driven with Garden, but the front-end/back-end gap meant that I was still not capturing my dream of truly functional CSS development (e.g. the front-end couldn’t send args to control the styles).
A passing remark on Twitter the other day implanted an idea: how about generating the styles on the front-end and sticking them into the <style>
element of the page? There are pros-and-cons to doing this; here are some I’ve thought about:
Pros | Cons |
---|---|
Functional (can take args) | |
Leverages existing hot-loading (Figwheel or Shadow) | |
Unlike backend Garden, allows easy name-spacing of all your styles | Requires extra for sharing styles between pages |
Renders styles without back-end calls for each css file | Defeats file caching |
Can work in conjunction with traditional back-end files | |
Is a middle-ground between maintenance-unfriendly in-line styles and static stylesheets |
After literally dreaming about this idea the other day, I spent an hour this afternoon implementing it, and it feels good! Once I’ve tried this for a while and have a better feel for the ins-and-outs, I might code it up as a micro-library; in the meanwhile, below is all the code involved.
The actual style definition (cljs, since Garden is all cljc):
(ns centrifuge.views.styles.article
(:require [garden.core :as garden :refer [css] ]
[garden.units :as u :refer [px em rem percent]]
[garden.color :as c :refer [hex->hsl hsl->hex hsl hsla]]))
(defn article
"Style for article page"
[{:keys [line-height]}] ;; This is where any args necessary to make the styles responsive would go
(css ;; css renders the datastructure to a string, not necessary for back-end Garden
[:h1 {:background "blue"
:line-height line-height}]))
Then the magic js interop to make it work:
(defn clear-styles!
"Remove existing style elements from the document <head>"
[]
(let [styles (.getElementsByTagName js/document "style")
style-count (.-length styles)]
(doseq [n (range style-count 0 -1)] ;; deleting backward avoids any funny-business because the HTMLCollection in `styles` is a live list and changes under our feet
(.remove (aget styles (dec n))))))
(defn mount-style
"Mount the style-element into the header with `style-text`, ensuring this is the only `<style>` in the doc"
[style-text]
(let [head (or (.-head js/document)
(aget (.getElementsByTagName js/document "head") 0))
style-el (doto (.createElement js/document "style")
(-> .-type (set! "text/css"))
(.appendChild (.createTextNode js/document style-text)))]
(clear-styles!)
(.appendChild head style-el)))
Then I just stick a call to (mount-style)
in your page element someplace and you’re set.
(mount-style (styles/article {:line-height 3))
Future Questions
-
Could I have actually reagent-mounted a style component into my
<head>
? Perhaps, which would have spared me some of that js interop, but then I would have clobbered all the other things already in the head (webjar assets, fonts and style links, session info, etc) -
I haven’t considered any cross-browser issues, besides willfully ignoring cautions that things are a touch more complicated on IE < 8.
-
There is still some thought available for where to put the
(css)
call that renders the text; I chose to put it in the style ns itself so that I don’t need to touch the dependencies of other namespaces, but there could be arguments to do it elsewhere.