I'm not sure how many of you are in this horrible situation where you have to write CSS for your React Components. I typically have to write way more than I'd like and always hated the way I wrote it. Until a few weeks ago.
I wrote this thing and so far I really like it. Too early to tell whether this is actually a good idea but I already prefer it over pretty much everything else I have used in the past (CSS, SCSS, OOCSS, BEM, ...).
Anyways here it goes:
(ns my.fancy.component
(:require [shadow.markup.css :as css :refer (defstyled)]))
(defstyled title :h1
[env]
{:color "red"})
(h1 {} "hello world")
In Clojure this produces <h1 class="my-fancy-component--title">hello world</h1>. There are also ways to generate the appropriate CSS so the element is actually styled in your page. Not totally settled on the final API but it works well enough for now.
In ClojureScript this produces a ReactElement and should work with React natively and most CLJS React Wrappers like OM (although I tried no other than my own). No extra CSS generation is required here, just include it in your page and it will be styled.
More here: https://github.com/thheller/shadow/wiki/shadow.markup
This is basically my take on the whole css-in-js thing that is happening in the JS world if anyone follows this. I wasn't happy with any of their implementations so I wrote this.
If you'd like to use this try it with this:
[thheller/shadow-client "1.0.180"]
The Clojure part also requires hiccup, the CLJS parts require React via cljsjs.react.
If anyone is actually interested in this I'd be happy to go over some more details. I just open-sourced this as I wanted to use it in another project and needed a place to put it. Consider this very ALPHA though, you have been warned. ;)
Cheers,
/thomas
The intent of shadow.markup (sorry, couldn't think of a better name yet) is that I basically never want to write a single selector ever again. There are several cases where this is still needed but way less than I usually do.
You get several things for free by combining the actual HTML Tag with its CSS.
This is a pretty good introduction to the topic of css-in-js and styled-components for React
https://www.youtube.com/watch?v=19gqsBc_Cx0
My API is very much inspired by this library, although less strings and more Clojure. There are many more implementations of this here: https://github.com/search?q=topic%3Acss-in-js&type=Repositories
Let me try with some sample Clojure code. You'd write something like this to generate a simple html snippet:
(ns my.site
(:require [hiccup.page :refer (html5))
(defn page-html [title body]
(html5
[:head]
[:body
[:div.box
[:h1.box__title "foo"]
[:div.box__content body]]]))
Here we invented tree css classnames that we need to remember here and whereever we get the actual CSS from. This over time leads to this: http://mrmrs.io/writing/2016/03/24/scalable-css/ at least it did for me in just about every project ever. Garden does not do anything in this regard I think, you still define things in two different places.
With shadow.markup you do this:
;; put all defstyled elements in a separate namespace in a .cljc, so you can use it everywhere.
(ns my.html.box
(:require [shadow.markup.css :as css :refer (defstyled)]))
(defstyled box :div
[env]
{:padding 10
:border "1px solid green"})
(defstyled title :h1
[env]
{:color "red"})
(defstyled contents :div
[env]
{})
;; my/site.clj
(ns my.site
(:require [my.html.box :as box]
[hiccup.page :refer (html5))
(defn page-html [title body]
(html5
[:head]
[:body
(box/box {}
(box/title {} title)
(box/contents {} body)))
]))
You do not need to remember whether to use a :h1 or :div, you just directly use the elements. Since everything in CLJ(S) is namespaced we get a safe naming scheme for CSS classes for free as well. You just write normal CLJ(S) code, you don't need to context switch and synchronize the class names back and forth. Refactoring the defstyled name in Cursive will rename all of its uses as well the CSS classnames. The CLJS version with :advanced gives you dead code removal for free, so if you don't use an element it's CSS will be removed as well.
There are many more things but the basic idea is to bundle the CSS with the HTML that uses it while still supporting all of CSS (ie. no inline styles) and remaining pure Clojure.
HTH,
/thomas
This is interesting. A few thoughts...
Have you considered using garden syntax for the style generation? I have a ton of garden styles already, and I imagine others do to, direct porting would be awesome.
Is there a way to reuse css classes? Generating classes for each element potentially produces a lot of redundant css, and in cases where performance is an issue, the same class being applied to many elements will be faster
As for re-using css classes: Don't! That is the whole point of this, as mentioned here: http://mrmrs.io/writing/2016/03/24/scalable-css/ and elsewhere.
I have been in this situation many times where I'm scared to touch an existing class since I have no direct record of who uses it in which situation. Or where I added another class with !important, just to have a quick fix somewhere. Also I hated OOCSS with things like <div class="foo bar baz boom bang fiddle"> where the actual order mattered and I never could remember which it was.
You can and should however abuse all the power of Clojure. I have this for some of my styles:
(defn button-style [env]
{:padding 10
:border (str "1px solid " (-> env :colors :border))
:background-color (-> env :colors :bg)})
and then re-use this a couple time for anything that should look like a button:
(defstyled link :a
[env]
(merge
(button-style env)
{:display "inline-block"
:text-decoration "none"}))
(defstyled button :button
[env]
(merge
(button-style env)
{:you-get "the-idea"}))
I currently still have a base.css which includes normalizr.css and some other generic classes, you can of course still use (button {:className "generic"} ...). Nothing wrong with that.
As for performance: The generated selectors are very specific in that they only match one thing, too open generic styles tend to cause issues. Also if you stick to "modern" things (flexbox) the need for some filler HTML elements goes away which improves overall performance, not just CSS. Style generation also happens ONCE, so even complex style-fns should not be an issue ever.
Also thanks for :advanced optimizations the code to generate the CSS should by much smaller than any actual minified CSS. Did I mention dead code removal yet? ;)
But I currently have only 89 "defstyled" things in my project myself, as this is still fairly new. Too early to make general claims about performance, but it should probably be on par with hand-written CSS. Generating styles on the client might be a bad idea too, my stuff is not yet complex enough to say. Time will tell, I am committed however as the past few weeks produced basically zero headaches about CSS and that is a nice feeling.
/thomas
Basically works out of the box:
https://github.com/thheller/reagent-test
The dependency on Clojure 1.9 was not intentional and I would remove it if anyone wants to use this with 1.8.
Anyways feel free to clone the test project and mess around, you get the whole figwheel experience + CSS without editing CSS files.
/thomas
(defstyled h1 :h1
[_]
{:color "red"
"@media (max-width: 600px)"
{:color "green"}})
Or put the query inside a def, so you can re-use it.
Alternatively you could use the CSS env as well.
(css/set-env!
{:media
{:smartphone
"@media (max-width: 400px)"}})
(defstyled h1 :h1
[env]
{:color "red"
(-> env :media :smartphone)
{:color "green"}})
I'm not too happy with the set-env! call as it must be called before the first element is USED (not defined). I call this in my app/init function. Didn't talk too much about env yet, but that is basically it. Define anything you want in env, it is passed into every defstyled when the first element is created and can influence your CSS that way (ie. theming support for re-usable components).
Cheers Thomas. When you have the likes of a containing element. How do you handle the wrap when defstyled returns a ReactElement?
(defstyled container :div
[_]
{:display "flex"})
Not sure I understand the question? ReactElements can have children, which this fully supports.
(defstyled things :div
[_]
{:display "flex"})
(defstyled thing :div
[_]
{:flex 1
"&.wide"
{:flex 2}})
(things {}
(thing {} "one")
(thing {:classes ["wide"]} "two")
(thing {} "three"))
If you are talking about styles that should only apply when B is contained in A you can do this
(defstyled A :div
[_]
{})
(defstyled B :div
[_]
{:normal "styles"
A
{:inside-a "styles"}})
I just relaxed the requirements for nesting a bit, previously you had to wrap it in a vector and supply a suffix.
(defstyled B :div
[_]
{:normal "styles"
[A ""]
{:inside-a "styles"}})
But with [thheller/shadow-client "1.0.183"] you can do the above version without the vector. The suffix is so you can refer to parent selectors as well.
I have used this once in my project
(defstyled control-group :div
[_]
{:margin-bottom 20
"&.inline"
{:display "flex"}
"&:last-child"
{:margin-bottom 0}
})
(defstyled control-label :div
[_]
(css/root
{}
(css/rule "&.select"
{:line-height 37})
(css/rule "&.bold"
{:font-weight "bold"})
(css/nested-rule [control-group ".inline"]
{:flex 1})))
(css/...) is the little more verbose syntax, which the maps desugar too. Could have used a map, wrote that before I had the map syntax.
The result of this is (assuming "test" ns):
div.test--control-group {...}
div.test--control-group.inline {...}
div.test--control-label {...}
;; and the nested rule:
div.test--control-group.inline div.test--control-label {..}
Hope that answers your question.
/thomas
Thanks got it. My mistake was mixing reagent's markup with yours. Would you say this is an alternative, similar to how shadow.markup relates to om.dom?
After seeing this message, we extracted a library from work that uses garden and adds the style when the component is rendered (when using React). Code currently lives at https://github.com/guilherme-teodoro/stylish
Usage is like:
(ns stylish.example
(:require [stylish.core :as stylish]
[garden.units :as units]))
(defn style
[color]
[{:font-size (units/px 40)
:background color}])
(def button-style
[{:color :yellow}
[:&:hover {:color :blue}]])
(defn show
[]
[:div {:class (stylish/render (style color))} ;; return: stylish_example-style-1
[:button {:class (stylish/render button-style)}]]) ;; return: stylish_example-button-style-1
We have being using it in prod from months now and it works fine.
HTH,
mynomoto
Yes. While I like the hiccup-ish reagent syntax it does present some performance issues.
In reagent you first allocate a bunch of vectors, these are then interpreted by reagent and turned into ReactElements allocating even more objects. The vectors are now "garbage" and have to be collected. The reagent approach produces about twice as much garbage and is slower overall (keyword parsing) as you have way more work being done each render, since browsers are pretty fast these days you don't notice this much but the price is there.
om.dom or shadow.markup directly go through React.createElement and the result will then be interpreted by React effectively skipping the "vector-phase". I even go a little further than om.dom if you look at the shadow.markup.react ns (direct drop-in replacement btw). I do some macro magic to further reduce cost. This is probably overkill though, but you get the usual dom/h1, dom/div, etc functions.
I think the reason why the hiccup syntax is so popular is because of the shorthand form of :div.class-a.class-b, but since you do not need this with defstyled the argument for the hiccup syntax is weaker.
So yes, the hiccup-ish syntax is an alternative but I stick to exclusively using the fn-style syntax. Using Cursive indent-only mode it indents just like vectors and after doing this for a while now it looks better than the hiccup syntax to me. The price of reagent is that you cannot mix both syntaxes, on the clojure version I currently support the mixed style but that is probably going away at some point.
Sorry, didn't want to turn this into a rant on reagent.
Cool, looks very much like
https://github.com/cssinjs/jss
https://github.com/css-modules/css-modules
Great to see some more work on this topic, feels like JS world has been on this for much longer. CLJS definitely needs to catch up here.
@Thomas Can you comment on the tradeoff having the CSS put inside the html page? In past projects we had strict rules not to put any CSS inside html files to allow the browser to cache them. I guess the scenario is different here since the CSS would be part of the Javascript send to the browser and cached along with the JS and then injected into the rendered page. Is that correct?
Another thought, my guess is the created css which is component based is much simpler in terms of used css selectors (because it applies to just the component) so it would be faster for the browser to render them compared to large style files containing nested rule sets- right?
Thanks,
Torsten.
Anyways here is the current state with regards to performance:
CSS is generated by the compiled JS at runtime.
Pros:
- No extra render-blocking <link> tag to fetch styles
- Dead-code removal also removes unused styles
- JS caching equals CSS caching.
- CLJS representation of CSS is probably smaller than actual CSS in total bytes used (ie. less for the user to download, need more data to verify though)
Cons:
- CSS is generated at runtime, this takes time.
For my current project it amounts to about 25ms total, spread out over time. Since styles are generated when they are needed, this can mean 5ms now and 20ms when the user clicks a thing (maybe never). I have a solution in place to generate the styles on the server but that is not dead-code aware (ie. all styles are generated, not just the ones you use). Working through some ideas to make this dead-code aware.
Cannot really comment on the performance of the CSS selectors, they should be faster than nested rules but it is surprisingly hard to find actual good benchmarks for this topic. Most of the information I could find is 2+ years old and may not be relevant anymore. The conclusion seems to be that the the selectors generated by shadow.markup should be ideal.
I can tell you with a bit of confidence that my solution is much faster than any other css-in-js solution I looked at (styled-components, fela, aphrodite and more I can't remember). Granted some of them are also shipping PostCSS which isn't exactly fast and quite heavy on the download size.
I am committed as the fallback has always been to generate the styles on the server. If everything else fails you still get a .css file you can use normally. So far the client-side story looks promising though. Writing the styles in CLJS has lessened my hate for CSS considerably. It is too early to tell whether it will accumulate as much waste as traditional methods over time. My instinct feels good about it though. Cursive already tells me whether or not I'm using an element by just showing me whether a "def" is used or not.
YMMV
/thomas