How are you styling your ClojureScript apps?

What CSS options have you had most success with for your ClojureScript sites and apps?

I’m surveying the styling landscape for ClojureScript apps and we’re spoilt for choice:

  1. Plain CSS
  2. Inline CSS (adding styles in hiccup :style values).
  3. CSS frameworks such as Tailwind and Bootstrap (adding framework class names to hiccup :class values, then doing something to minifiy/shake unused CSS rules in production).
  4. Sass/PostCSS.
  5. CSS-in-CLJS
  6. Garden

If you want to add some nuance to the responses, feel free to mention:

  • What are you building? (Static sites? SPAs? Mobile apps? Add a link if you wish.)
  • How many people are you building it with?
  • How does your build process work, if you have one? (Links to repo are welcome.)
3 Likes
  • What are you building? (Static sites? SPAs? Mobile apps? Add a link if you wish.)

A static site.

  • How many people are you building it with?

Just me

  • How does your build process work, if you have one? (Links to repo are welcome.)

It’s a small little personal site.

  • What CSS options have you had most success with for your ClojureScript sites and apps?

I use this CSS-in-CLJS solution: GitHub - clj-commons/cljss: Clojure Style Sheets — CSS-in-JS for ClojureScript

1 Like

I’m so glad you asked this question, I’ve been walking around with an answer in my head! :slight_smile:

(I would also love to read a common-sense explanation of what other people are doing for this same problem.)

:+1: for TailwindCSS and another one :+1: for TailwindUI.

My workflow is basically the following:

  1. Look for something on Tailwind UI that’s similar to what I need to do.
  2. Copy the HTML for that into my clipboard
  3. Paste it into this html->hiccup converter tool online.
  4. Paste the hiccup into my cljs files,
  5. wrap it in a defn and
  6. modify in-place until it both works and does the job.

Example input HTML with TailwindCSS

        <div class="w-0 flex-1 flex">
          <a href="mailto:[email protected]" class="relative -mr-px w-0 flex-1 inline-flex items-center justify-center py-4 text-sm text-gray-700 font-medium border border-transparent rounded-bl-lg hover:text-gray-500">
            <!-- Heroicon name: solid/mail -->
            <svg class="w-5 h-5 text-gray-400" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true">
              <path d="M2.003 5.884L10 9.882l7.997-3.998A2 2 0 0016 4H4a2 2 0 00-1.997 1.884z" />
              <path d="M18 8.118l-8 4-8-4V14a2 2 0 002 2h12a2 2 0 002-2V8.118z" />
            </svg>
            <span class="ml-3">Email</span>
          </a>
        </div>

Hiccup ready for cljs

([:html
  [:body
   [:div.w-0.flex-1.flex
    [:a.relative.-mr-px.w-0.flex-1.inline-flex.items-center.justify-center.py-4.text-sm.text-gray-700.font-medium.border.border-transparent.rounded-bl-lg.hover:text-gray-500
     {:href "mailto:[email protected]"}
     "<!-- Heroicon name: solid/mail -->"
     [:svg.w-5.h-5.text-gray-400
      {:aria-hidden "true", :fill "currentColor", :viewbox "0 0 20 20"}
      [:path
       {:d
        "M2.003 5.884L10 9.882l7.997-3.998A2 2 0 0016 4H4a2 2 0 00-1.997 1.884z"}]
      [:path
       {:d "M18 8.118l-8 4-8-4V14a2 2 0 002 2h12a2 2 0 002-2V8.118z"}]]
     [:span.ml-3 "Email"]]]]])

A little bit of cleanup is unfortunately required …

but I like that this method creates beautiful UI’s that work well because they’re functional. (The typical OO UI-component stuff is wasted on me.)

  1. :viewbox in the SVG icon is incorrect and needs to become something that react/reagent likes.
  2. The HTML comment becomes a string in the middle of your hiccup and should be removed.
  3. etc.

Updates to the components are interesting, I usually just repeat the whole process from scratch…

2 Likes

@didibus Thank you for mentioning cljss. I had seen it but wasn’t sure of its status because the linked homepage appears to be offline. I’ll have to give it a try.

@pieterbreed Thanks for sharing your approach! Do you point the tailwind config file at your CLJS to purge unused styles?

I can see what you mean about updates/maintainability potentially being an issue (as well as readability, I suppose?). I like how fast Tailwind can be to prototype with, but I also appreciate the slightly larger separation of content and style that CSS-in-JS can give you.

1 Like

To be honest, I don’t have a good answer for purging the unused styles. I’ve been thinking about putting those hiccup defn’s in cljc files, and combining with spec/gen to generate HTML to be used for purging. This is still only an idea in my head though. :man_shrugging: For the project I shipped with this approach, the enormous CSS wasn’t a problem because it never changes and the browser caches were fine with it after the first load.

It might just be me, but I feel like this is something that happens a lot with LISP-based systems. Big once-off loading costs, then efficiencies get better as you use the software over time.

1 Like

I’m using TailwindCSS with PostCSS. With some little tweakings, everything works smoothly.

Purge from hiccup

In order to purge according to your Clojure code, You can modify purge.options.defaultExtractor in tailwind.config.js to something like

/[^<>"'.`\s]*[^<>"'.`\s:]/g

Example tailwind.config.js

module.exports = {
  mode: 'jit',
  purge: {
    content: ['./src/cljs/**/*.cljs',
              './test/cljs/**/*.cljs'],
    options: {
      defaultExtractor: content => content.match(/[^<>"'.`\s]*[^<>"'.`\s:]/g)
    }
  },
  variants: {},
  plugins: [
    require('@tailwindcss/forms')
  ],
  }
}

Call PostCSS in build hook

I have a postcss.clj which provide tasks for Shadow CLJS.

;; postcss.clj
(ns postcss
  (:require
   [babashka.process :as proc]))

(defn watch
  {:shadow.build/stage :configure}
  [build-state src dst]
  (proc/process ["./node_modules/.bin/postcss" src "-o" dst "--verbose" "-w"]
                {:env {"TAILWIND_MODE" "watch"}})
  build-state)

(defn release
  {:shadow.build/stage :configure}
  [build-state src dst]
  (proc/process ["./node_modules/.bin/postcss" src "-o" dst "--verbose"]
                {:env {"NODE_MODE" "production"}})
  build-state)

Use them in shadow-cljs.edn via build hooks.

:dev        {:build-hooks [(postcss/watch "src/css/app.css" "resources/public/css/app.css")]}
:release    {:build-hooks [(postcss/release "src/css/app.css" "resources/public/css/app.css")]}

Limitations

You can’t use CSS classes like w-1/3 in hiccup tag keyword, because the slash has special meaning. Use something like {:class ["w-1/3"]}

7 Likes

For some toy projects I used material-ui which was pretty easy to use and made for a nice looking app.

I never really made a custom theme or other customizations to make it look different, so maybe if you don’t want the material-ui look&feel something else is better.

I made a demo where I translated the material-ui templates from html to hiccup here: GitHub - dakra/mui-templates: material-ui templates in hiccup with reagent, re-frame, shadowcljs, reitit
It’s compiled with shadow-cljs and automatically deployed to GitHub Pages with GitHub Actions.

1 Like

For Reagent apps I recommend Re-com https://re-com.day8.com.au/#/introduction

It’s based on bootstrap and I really like the layout system. (simple & easy)

I used it for my tiny IPTV app Glotze

1 Like

Some feedback from @thheller on spawning processes from shadow’s build hook:

FWIW I strongly recommend NOT running secondary tools in hooks. especially not something that starts processes that keep running

prefer setups like this one https://github.com/jacekschae/shadow-cljs-tailwindcss/blob/main/package.json#L8 using npm-run-all to run multiple things but in separate processes

running separate processes is not what build-hooks are made for and should be avoided

Also check out babashka tasks for spawning multiple processes in your development.