Skip to content
/ stylefy Public

Clojure(Script) library for styling user interface components with ease.

License

Notifications You must be signed in to change notification settings

Jarzka/stylefy

Repository files navigation

stylefy logo

Clojure(Script) library for styling user interface components with ease.

Clojars Project CircleCI

API documentation

Introduction

stylefy makes it possible to define CSS styles as Clojure data and attach them into HTML elements easily without writing selectors. Styles are converted to CSS on-demand and scoped locally. This makes writing style code easy and maintainable.

While being originally created with the frontend in mind, stylefy now runs on both Web browsers and servers. stylefy uses Garden in the background for most of its CSS conversions.

Features

  • Define styles as Clojure data – all important CSS features are supported: pseudo-classes, pseudo-elements, keyframes, font-faces, media queries, feature queries...
  • Use any 3rd party CSS code along with stylefy
  • Manual mode: stylize 3rd party components and resolve corner cases in which complex CSS selectors are needed
  • Scoping: define styles that are applied only when the current element is in some specific scope
  • Vendor prefixes are supported both locally and globally
  • CSS caching on the frontend using local storage (optional)
  • Small and simple core API which is easy to setup
  • Fast, asynchronous CSS generation on the frontend (with an option to use synchronous generation)
  • Automatic style reloading (with shadow-cljs or Figwheel)
  • Server Side Rendering
  • All features are tested to work with Chrome, Firefox, Edge & Internet Explorer 11

Installation

stylefy consists of modules that are optimised for specific UI libraries / frameworks.

First, add stylefy core as a dependency:

[stylefy "3.2.0"]

Then, based on the UI library you are using, add a corresponding module. The main purpose of the module is to handle asynchronous DOM updates when components are rendered.

Reagent

[stylefy/reagent "3.0.0"]

Rum

[stylefy/rum "3.0.0"]

Other

If your UI library is not listed above, use the generic module:

[stylefy/generic-dom "3.0.0"]

This module makes only synchronous DOM updates and is not optimised for any specific UI library. It can also be used with Reagent & Rum, but it's generally slower.

Setup

Frontend (ClojureScript)

Follow these steps if you want to render styles on the browser:

Add the following style tags on your page's head tag. It is recommended that these tags are the last style tags in the header so that it is less likely that possible other styles would override them.

The first tag is going to contain CSS definitions that are not going to change (font-face, keyframes etc.). The second will contain class definitions that are added into the DOM on-demand when components need them.

<style id="_stylefy-constant-styles_"></style>
<style id="_stylefy-styles_"></style>

Require stylefy along with the module:

(:require [stylefy.core :as stylefy]
          [stylefy.reagent :as stylefy-reagent])

Then, call stylefy/init once when your application starts. This function must be called before rendering any styles on the page.

(stylefy/init {:dom (stylefy-reagent/init)})

Now you are ready to go! But before you start, it's good to be aware of a few things:

Caching summary

stylefy supports style caching for styles generated on the frontend via HTML5 local storage. The converted CSS code is added into local storage and loaded from there when the page is reloaded. Caching can speed up the style generation, but it also consumes space from the local storage. Caching is turned on by default, but it is up to you to consider whether you want to keep it that way.

You can read more about style caching in the Style caching section.

Multi-instance summary

If you are planning to run multiple apps using stylefy on the same web page / domain, the setup needs additional steps. See: Multi-instance support section.

Rum summary

For Rum users only: You must always have rum/reactive mixing in components using stylefy.

Backend (Clojure)

Follow these steps if you want to render styles on the server. These steps are not necessary if you only render styles on the frontend.

Call (stylefy/init) once when your server starts.

Assuming you have a query function which generates HTML text, wrap it with stylefy/query-with-styles.

(stylefy/query-with-styles
  (fn [] (html (example-component))))

If use-style, or some other stylefy function, is called during the query execution the generated CSS is kept in temporary memory. When the query has finished, a special value of _stylefy-server-styles-content_ is searched in the output and replaced with the generated CSS. The end result is HTML code with styles attached.

Full example:

(ns my.app
  (:require [hiccup.core :refer [html]
            [stylefy.core :as stylefy :refer [use-style]]))

(defn add-font-face []
  (stylefy/font-face {:font-family "open_sans"
                      :src "url('../fonts/OpenSans-Regular-webfont.woff') format('woff')"
                      :font-weight "normal"
                      :font-style "normal"}))

(defn index []
  [:html
   [:head
    [:title "Example"]

    [:style {:id "_stylefy-server-styles_"} "_stylefy-server-styles-content_"] ; Generated CSS will be inserted here
    [:style {:id "_stylefy-constant-styles_"}]
    [:style {:id "_stylefy-styles_"}]]

   [:body (use-style {:color :black})
    [:div (use-style {:text-align :center} {:id "app"})]]])

(defn example-query []
  ; stylefy must have been initialised at this point
  (stylefy/query-with-styles
    (fn [] 
      (add-font-face)
      (html (index)))))

If you wish to do something else with the generated CSS, you can create your own execution context. Use the implementation of query-with-styles as an example.

Usage

Creating & using styles

Create a style as a normal Clojure map:

(def button-style {:padding "2rem"
                   :background-color "#BBBBBB"
                   :border "0.5rem solid black"})

To use it in a component, use the use-style function:

(defn- button [text]
  [:div (use-style button-style)
    text])

use-style accepts HTML attributes as the second parameter:

(defn- button [text]
  [:div (use-style button-style {:on-click #(.log js/console "Click!")
                                 :class "some-3rd-party-button-class"})
    text])

On the frontend, use-style adds the style into the DOM as a CSS class on-demand (see How does the CSS generation work on the frontend? for more details). On the server, it returns a class name pointing to the generated CSS code.

Passing styles to components

use-style is designed to be called only inside component render functions to define styles for HTML elements. If you need to pass styles to Reagent components, pass them as regular Clojure maps, and call use-style last, only for HTML elements:

(defn- button-with-custom-style [text style]
   [:button (use-style style {:on-click #(.log js/console "Click!"))
     text])
     
(defn- button-wrapper []
  [:div
    ; This is OK
    [button-with-custom-style "Hello" {:padding "1rem"
                                       :background-color "#BBBBBB"
                                       :border "0.5rem solid black"}]
    ; This is NOT ok, because use-style would be called twice, second time with incorrect arguments
    [button-with-custom-style "Hello" (use-style {:padding "1rem"
                                                  :background-color "#BBBBBB"
                                                  :border "0.5rem solid black"})]])

Combine & parametrise styles

Combine or parametrise styles however you like:

; Define a style using some other style as a template
(def primary-button (merge generic-button {:background-color "rgb(88, 121, 193)"}))

; Generate a style via function
(defn custom-primary-button-style [color]
  (merge primary-button {:color color}))

(defn custom-button [text color]
  [:button (use-style (custom-primary-button-style color))
    text])

Notice that if your style-generator function takes numeric values, and you know that it's going to be called with numerous different parameter combinations, it is recommended to use these numeric values via inline styles. Otherwise every possible number combination creates a new CSS class, which is inefficient. So instead of this:

(defn button-with-dynamic-position [text color x y]
  ; Bad! Every possible parameter combination creates a new CSS class!
  [:button (use-style (positioned-button color x y))
    text])

Do something like this:

(defn button-with-dynamic-position [text color x y]
  [:button (merge (use-style (custom-primary-button-style color))
                  {:style {:left x :top y}})
    text])

Modes (pseudo-classes & pseudo-elements)

stylefy modes are pretty much the same thing as pseudoclasses/pseudoelements in CSS and they simply create a new "class:mode" selector for your style. The reason for not using the name pseudoclass is completely self-willed; I think "mode" as a name is a little bit more informative than CSS pseudoclasses.

Here is an example of how to define a style with modes:

(def style-with-modes {:background-color "rgb(88, 121, 193)"
                       ::stylefy/mode {:hover {:background-color "rgb(98, 131, 213)"}
                                       :before {:content "'CSS generated content'"}
                                       "::-webkit-progress-bar" {:-webkit-appearance "none"}}})

In some cases the order of CSS pseudo elements is important, so writing modes in vector format is also supported:

(def style-with-modes {:background-color "white"
                       ::stylefy/mode [[:before {:content "'CSS generated content'"}]
                                       [:hover {:background-color "#ffedcf"}]
                                       [:active {:background-color "blue" :color "white"}]]})

Sub-styles

Define a style for your component and all the elements inside of it in a single map:

(def list-container-style (merge generic-container
                                 {::stylefy/sub-styles {:list {:margin-top "1em"}
                                                        :list-item {:color "black"}}}))

(defn list-in-container []
  [:div (use-style list-container-style)
   [:ul (use-sub-style list-container-style :list)
    [:li (use-sub-style list-container-style :list-item) "List element 1"]
    [:li (use-sub-style list-container-style :list-item) "List element 2"]
    [:li (use-sub-style list-container-style :list-item) "List element 3"]]])

Another version using deeper sub-style nesting:

(def list-container-style (merge generic-container
                                 {::stylefy/sub-styles
                                   {:list {:margin-top "1em"
                                           ::stylefy/sub-styles {:item {:color "black"}}}}}))

(defn list-in-container []
  [:div (use-style list-container-style)
   [:ul (use-sub-style list-container-style :list)
    [:li (use-style (sub-style list-container-style :list :item)) "List element 1"]
    [:li (use-style (sub-style list-container-style :list :item)) "List element 2"]
    [:li (use-style (sub-style list-container-style :list :item)) "List element 3"]]])

Sub-styles are nothing special, they are supposed to contain the same contents as the main style map. If you wish, you can always omit them and use the regular use-style without any sub-styles. However, sub-styles help you to define styles that are closely related to each other in a single map, which can make style maintenance easier.

Vendor prefixes

Supported in the same way as Garden supports them.

(def button {:border "0.5rem solid black"
             :border-radius "0.5rem"
             ::stylefy/vendors ["webkit" "moz" "o"]
             ::stylefy/auto-prefix #{:border-radius}})

You can also use globally defined vendor prefixes. These prefixes are automatically added into every style map.

(stylefy/init {:global-vendor-prefixes {::stylefy/vendors ["webkit" "moz" "o"]
                                        ::stylefy/auto-prefix #{:border-radius}}})

Media queries

Define how your style looks on various screen sizes:

(def phone-width "26rem")

(def column {:padding "0.5rem"
             :color "white"})

(def responsive-layout {:display :flex
                        :flex-direction :row
                        ::stylefy/media {{:max-width phone-width} {:flex-direction :column}}
                        ::stylefy/sub-styles {:column1 (merge column
                                                              {:background-color "#AA0000"
                                                               :flex 1})
                                              :column2 (merge column
                                                              {:background-color "#00AA00"
                                                               :flex 2})
                                              :column3 (merge column
                                                              {:background-color "#0000AA"
                                                               :flex 1})}})

(defn responsive-layout []
  [:div (use-style responsive-layout)
   [:div (use-sub-style responsive-layout :column1)
    [:p "This is column 1"]]
   [:div (use-sub-style responsive-layout :column2)
    [:p "This is column 2"]]
   [:div (use-sub-style responsive-layout :column3)
    [:p "This is column 3"]]])

Alternative version using vector syntax. This is useful if the order of the rules matter in CSS.

{::stylefy/media [[{:max-width phone-width} {:flex-direction :column}]
                  [{:max-width tablet-with} {:flex-direction :row}]]}

stylefy features supported in media queries: modes, scope and vendor prefixes.

For syntax help, see Garden's documentation.

Feature queries

Define how your style looks when certain CSS features are supported by the browser:

(def grid-style {; Default style uses Flexbox as fallback
                 :display "flex"
                 :flex-direction "row"
                 :flex-wrap "wrap"
                 ::stylefy/media {{:max-width styles/phone-width}
                                  {:display "block"}}
                 ; Use CSS Grid style if it is supported by the browser.
                 ; If the browser does not support CSS Grid or feature queries at all, this
                 ; block is simply ignored.
                 ::stylefy/supports {"display: grid"
                                     {:display "grid"
                                      :grid-template-columns "1fr 1fr 1fr"
                                      ; Make CSS Grid responsive
                                      ::stylefy/media {{:max-width styles/phone-width}
                                                       {:grid-template-columns "1fr"}}}}})

Alternative version using vector syntax. This is useful if the order of the rules matter in CSS.

{::stylefy/supports [["display: flex" {:color :green}]
                     ["display: grid" {:color :purple}]]}

3rd party classes

You can use 3rd party class names by passing them via :class HTML attribute. The value can be a string, a keyword or a vector of strings/keywords.

(defn- bs-navbar-item [index index-atom text]
  [:li (use-style styles/clickable
          {:class (when (= @index-atom index) "active")
           :role "presentation"
           :on-click #(reset! index-atom index)})
   [:a text]])

Alternative syntax:

(defn- bs-navbar []
  (let [active-index (r/atom 0)]
    (fn []
      ; Additional classes can also be attached in the name of the element
      [:ul.nav.nav-pills (use-style styles/boostrap-navbar-overrides)
       [bs-navbar-item 0 active-index "One"]
       [bs-navbar-item 1 active-index "Two"]
       [bs-navbar-item 2 active-index "Three"]
       [bs-navbar-item 3 active-index "Four"]])))

3rd party classes can also be attached directly into a style map. This means that the defined additional class names are always used with the style. It accepts the same syntax as :class.

(def boostrap-navbar {:background-color "#DDDDDD"
                      ::stylefy/with-classes ["nav" "nav-pills"]})
                      
(defn- bs-navbar []
  (let [active-index (r/atom 0)]
    (fn []
      [:ul (use-style boostrap-navbar)
       [bs-navbar-item 0 active-index "One"]
       [bs-navbar-item 1 active-index "Two"]
       [bs-navbar-item 2 active-index "Three"]
       [bs-navbar-item 3 active-index "Four"]])))

Font-face

Call stylefy/font-face and the given font-face is added into the DOM as CSS code asynchronously (frontend) or into the current execution context (backend).

(stylefy/font-face {:font-family "open_sans"
                    :src "url('../fonts/OpenSans-Regular-webfont.woff') format('woff')"
                    :font-weight "normal"
                    :font-style "normal"})

Keyframes

Call stylefy/keyframes and the given keyframes are added into the DOM as CSS code asynchronously (frontend) or into the current execution context (backend).

(stylefy/keyframes "simple-animation"
                   [:from
                    {:background-color "red"}]
                   [:to
                    {:background-color "blue"}])

(def animated-box (merge simple-box
                         {:animation-name "simple-animation"
                          :animation-duration "3s"
                          :animation-iteration-count "infinite"}))

Custom class names

As has been told, stylefy converts style definition to unique CSS classes automatically and there is no need to worry about class names. It can, however, be useful to be able to generate custom named classes for example when working with 3rd party libraries / frameworks. For this purpose, call stylefy/class:

; This generates a CSS class with the name "background-transition" 
; and adds it into the DOM asynchronously (frontend) 
; or into the current execution context (backend).

(stylefy/class "background-transition"
               {:transition "background-color 1s"})

; Use the generated class in a component like any other class
[:div.background-transition]

Custom tag styles

You can generate styles for HTML tags by calling stylefy/tag:

; This generates a CSS tag selector and style for "body" element
; and adds it into the DOM asynchronously (frontend)
; or into the current execution context (backend).

(stylefy/tag "body" {:background-color :lightyellow
                     :padding          "1rem"})

Manual mode

Manual mode can be used to style child elements with manually written CSS selectors using Garden syntax. It can be useful when:

  • Complex CSS selectors are needed to achieve some behaviour
  • You are using some 3rd party CSS framework which has more precise CSS selectors than stylefy's autogenerated CSS class names, and you want to override them
  • You want to style some 3rd party (Reagent) components that do not take style props as parameters.

The selector and the style written in manual mode will be scoped inside the element in which you use the style map with use-style. To avoid creating ambiguous style maps, stylefy's special keywords cannot be used in manual mode.

An example of a situation when we need to use manual mode: we want to change the style of some child element when the parent element is being hovered.

(def mobile-media-query {:max-width "26rem"})

(def hoverbox-style
  {:width "26rem"
   :height "15rem"
   :padding "2rem"
   :margin-bottom "1rem"
   :background-color "#55AA55"
   ::stylefy/sub-styles {:innerbox {:width "100%"
                                    :height "100%"
                                    :background-color "#444444"}}
   ; Change the background color of the child element when the parent element is being hovered.
   ; This is a corner case that stylefy cannot handle directly, so we use manual mode to resolve it.
   ::stylefy/manual [; Since stylefy's special keywords do not work in manual mode,
                     ; we have to use Garden syntax as a replacement.
                     ; Syntax for pseudo-classes/elements:
                     [:&:hover [:.innerbox
                                ; Brighten by default
                                {:background-color "#999999"}]]
                     ; at-rules can be created with Garden's helper functions: at-media, at-supports...
                     ; (these can also be nested if necessary)
                     (at-media mobile-media-query [:&:hover [:.innerbox
                                                             ; Darker on mobile
                                                             {:background-color "#666666"}]])]
   ::stylefy/media {mobile-media-query
                    {:width "100%"}}})

(defn hoverbox []
  [:div (use-style hoverbox-style)
   [:div.innerbox (use-sub-style hoverbox-style :innerbox)]])

For syntax help, see Garden's documentation.

Notice that Garden's selector syntax can contain strings, so purely string-based selectors can also be used:

(def style
  {:color :red
   ::stylefy/manual [["> .box:hover" {:color "black"}]]})

Scoping

Scoping can be used to define styles that are applied only when the current element is in some specific scope. The scoping rule is written using Garden syntax:

(def style
  {:color "white"
   ::stylefy/scope [[:.scoped-box
                     ; These additional style definitions are applied only 
                     ; when the current element is inside of .scoped-box
                     {:color "red"
                      ::stylefy/mode {:hover {:color "yellow"}}
                      ::stylefy/manual [[:.green-text-in-scoped-box {:color "green"}]]}]]})

The style map inside the scoping rule is automatically wrapped in a vector with the name of the autogenerated class name, which creates the final scoping rule.

::stylefy/scope and ::stylefy/media can be used together like this:

(def style
  {:color "white"
   ::stylefy/scope [[:.scoped-box {:color "red"
                                   ::stylefy/manual [[:.special-text-in-scoped-box {:color "green"}]]}]]
   ::stylefy/media {{:max-width "26rem"}
                    {::stylefy/scope [[:.scoped-box {:color "blue"
                                                     ::stylefy/manual [[:.special-text-in-scoped-box {:color "purple"}]]}]]}}})

Or you can use media queries inside scoped style with the help of Garden's at-media helper:

(def style
  {:color "white"
   ::stylefy/scope [[:.scoped-box {:color "red"}]
                    [:.scoped-box (at-media {:max-width "26rem"} [:& {:color "blue"}])]]})

You can also use media queries inside ::stylefy/manual inside ::stylefy/scope:

(def style
  {:color "white"
   ::stylefy/scope [[:.scoped-box {:color "red"
                                   ::stylefy/manual [[:.green-text-in-scoped-box {:color "green"}]
                                                     (at-media {:max-width "26rem"} [:.green-text-in-scoped-box {:color "purple"}])]}]]})

stylefy features supported in scoped style map: modes, manual mode and vendor prefixes (must be defined in parent style map). However, notice that these features cannot be used when creating media queries with at-media.

For syntax help, see Garden's documentation.

Style caching (frontend)

stylefy supports style caching for styles generated on the frontend via HTML5 local storage. The converted CSS code is added into local storage and loaded from there when the page is reloaded. Caching can speed up the style generation, but it also consumes space from the local storage. Caching is turned on by default, but it is up to you to consider whether you want to keep it that way.

Caching can be turned off in the initialisation function:

  (stylefy/init {:use-caching? false})

By default, the cache is cleared in seven days. You can modify this with cache-options:

(stylefy/init {:use-caching? true
               :cache-options {:expires (* 1 60 60 24 7)}}) ; Cache is cleared automatically after 7 days

You can also clear the cache manually:

(require '[stylefy.cache :as stylefy-cache])
(stylefy-cache/clear)

Storage origin

If you run multiple stylefy apps on the same domain, they all share the same local storage cache as defined by the Same origin policy. This applies to apps developed on localhost domain too. If you want every app to have its own cache, either use a different domain / port for every app, or use Multi-instance support.

Multi-instance support (frontend)

Multi-instance support can be used in the following scenarios:

  • Running multiple apps using stylefy on the same web page (every app must be built separately).
  • Running multiple apps using stylefy on the same domain in a way that every app uses its own HTML5 local storage cache.

To setup multi-instance support, use your app name as a suffix in the style tag id.

<style id="_stylefy-constant-styles_myapp"></style>
<style id="_stylefy-styles_myapp"></style>

Then init stylefy with multi-instance support. Instance-id is a unique string (app name). Base-node is an optional base node for style tags (handy if you use web components).

(stylefy/init {:multi-instance {:base-node (dommy/sel1 "#myapp")
                                :instance-id "myapp"}})

Garden units and colors

You can use Garden's Unit and Color helpers with stylefy. You can also use Garden's defcssfn macro to create custom CSS functions (notice that defcssfn is a macro and needs garden.core dependency declaration).

Debugging and testing

If you want to test your user interface by examining CSS class names, stylefy's automatically generated class names can become a hassle. To make testing and debugging easier, you can use your own prefix in stylefy's automatically generated class names:

(def my-style {:color "red"
               ::stylefy/class-prefix "debugthis"}

Notice that you need to turn custom prefixes on separately on the init function:

(stylefy/init {:use-custom-class-prefix? true})

FAQ

How is this library different than Garden?

Garden is awesome, but it's "only" a Clojure to CSS generator. If you want to use Garden to style your page, you are pretty much going to write CSS code as usual, i.e. write classes and selectors to stylize things on the page. You also need to avoid CSS quirks like name conflicts and make sure you always handle CSS cascading process correctly. stylefy helps you with this; you just write your style definition in a map and attach it to your component in the render function by calling use-style. There is no need to write CSS classes or selectors, no need to worry about name conflicts, dead CSS code etc.

Yes, it is possible to easily attach styles to components with Garden too if you use inline styles. But if you use stylefy, all your style definitions are converted to unique CSS classes automatically and the corresponding class is attached to your component. This is more effective than using inline-styles, especially if the same component exists multiple times on the same page. The style is defined only once in the CSS class, not multiple times in each component instance. Also, some CSS features are not available to use as inline styles (pseudoclasses, media queries etc.). For stylefy, this is not a problem, as it allows you to define pseudoclasses and media queries within the style map and converts everything to CSS automatically.

TLDR; stylefy it's like using inline CSS, but with full support for all CSS features that are not normally available when using CSS inline.

How does the CSS generation work on the frontend?

Note: This information only applies to stylefy/reagent and stylefy/rum modules.

use-style saves the style and adds it into the DOM as a CSS class asynchronously (if it's not already there). The return value is a map containing the given style properties as inline style. Inline style is used for a very short time, until the CSS class has been generated and inserted into the DOM. When the DOM is ready, the component is forced to re-render itself and use only the CSS class definition.

You might ask why does use-style work asynchronously? Why don't we add the style into the DOM immediately and return its class name? The reason is speed. Consider a case when multiple components are being rendered and use-style is being called many times with different style maps. In this case, updating the DOM on every single call would slow the rendering process down. To keep the rendering fast, the idea is to collect as many style maps as possible during a single render event, convert all of them to CSS and add into the DOM at once.

If the style contains some specific definitions that cannot be present as inline style (some specific modes or media queries), the HTML element using the style is going to be hidden for a very short time with CSS visibility set to hidden, until the converted CSS style is added into the DOM. In most cases, this should not be a problem, but if needed, the style can be added into the DOM synchronously by calling prepare-style:

[:div (use-style (prepare-style style))]

Because prepare-style causes immediate synchronous DOM update, it is not recommended to overuse it, as it can slow the rendering process. Also, when caching is used, the style will be ready after its CSS has been created for the first time.

More examples

More examples available here: https://github.com/Jarzka/stylefy/tree/master/examples/

More cool stuff

  • Need to namespace or unnamespace keywords in a map? Checkout my other library: namespacefy
  • If you also want to present SQL queries as Clojure data, checkout specql