Repository: CSCfi/rems
Pull request #2655 (2021-04-29 11:31:29)

document existing theme variables in config-defaults.edn
I lifted all the theme vars we use, and their default values, into
config-defaults.edn. I tested this refactor by checking that screen.css
stays identical.

After this I did some small cleanups that shouldn't affect production themes.

There's now an error in dev-mode if you use (get-theme-attribute :foo)
without addding :foo to config-defaults.edn, so the situation should
stay good.

For #2588

# Checklist for author

Remove items that aren't applicable, check items that are done.

## Reviewability
- [x] Link to issue

## Backwards compatibility
- [x] Config is backwards compatible

## Documentation
- [x] Update changelog if necessary
- [x] New config options in config-defaults.edn

## Follow-up
- [x] New tasks are created for pending or remaining tasks

opqdonut: I was testing that the dev more, i.e. example-theme css stays the same. Looks like the default theme css has changed and that leads to accessibility violations. I'll try to fix it so that the default css stays the same as well.

Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index af194e183..c62c8a072 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -10,6 +10,15 @@ Changes since v2.17
 
 ### Changes
 - The "Attachments (zip)" button in the UI now only downloads the current application attachments. Event attachments and previous versions of application attachments are left out. The full zip is still available via the API. (#2453)
+- Changes to theming: (#2588)
+  - Theme variables are now documented in [resources/config-defaults.edn](resources/config-defaults.edn).
+  - The `:nav-color` now simply defaults to `:link-color`. Previously, it defaulted to `:color3` if `:link-color` is unset.
+  - The theme variables `:danger-color` (didn't really affect anything) and `:phase-background-active` (wasn't used, overlaps with `:phase-bgcolor-active`) have been removed.
+  - The default theme has minor visual changes:
+    - color2 is lighter
+    - table hover highlight is now dark-on-light instead of light-on-dark
+    - link color used in nav bar
+  - Some theme-related code was rewritten. There should be no changes to appearance, but bugs are possible.
 
 ### Fixes
 - Errors for invalid inputs (field values that are too long, invalid email addresses, etc.) are now rendered nicely. Previously the applicant just saw a "Save draft: Failed" message. (#2611)
diff --git a/resources/config-defaults.edn b/resources/config-defaults.edn
index 428b1081f..b1f6e99e0 100644
--- a/resources/config-defaults.edn
+++ b/resources/config-defaults.edn
@@ -121,9 +121,118 @@
 
  ;; UI theme variables.
  :theme {:color1 "#eee"
-         :color2 "#555"
+         :color2 "#999"
          :color3 "#000"
-         :color4 "#000"}
+         :color4 "#000"
+
+         ;; Images & logos
+         :img-path  "../../img/" ; Path for static images, relative to /css/<lang>/screen.css
+         :logo-name nil ; Big logo, used on home page. Also used on other pages if :navbar-logo-name is not set.
+         :logo-name-en nil ; Localized big logo, defaults to :logo-name
+         :logo-name-sm nil ; Small version of big logo, used in mobile mode.
+         :logo-name-en-sm nil ; Localized small version of big logo, defaults to :logo-name-sm
+         :navbar-logo-name nil ; Small logo for navbar. Used on non-home pages instead of :logo-name.
+         :navbar-logo-name-en nil ; Localized small logo for navbar, defaults to :navbar-logo-name
+         :logo-bgcolor nil
+         :logo-content-origin nil
+
+         ;; Phases
+         :phase-bgcolor "#eee"
+         :phase-color "#111"
+         :phase-bgcolor-active "#ccc"
+         :phase-color-active nil ; defaults to :phase-color
+         :phase-bgcolor-completed "#ccc"
+         :phase-color-completed nil ; defaults to :phase-color
+
+         ;; Navbar
+         :button-navbar-font-weight 700
+         :navbar-color "#111" ; color of text in navbar
+         :nav-color nil ; color of links on navbar, defaults to :link-color
+         :nav-active-color nil ; defaults to :color4
+         :nav-hover-color nil ; defaults to :color4
+         :big-navbar-text-transform "none" ; can be used to e.g. uppercase the navbar text
+
+         ;; Tables
+         :table-hover-color nil ; defaults to :table-text-color
+         :table-hover-bgcolor nil ; defaults to :color2
+         :table-bgcolor nil ; defaults to :color1
+         :table-shadow nil
+         :table-border "1px solid #ccc"
+         :table-heading-color "#fff"
+         :table-heading-bgcolor nil ; defaults to :color3
+         :table-selection-bgcolor nil ; defaults to a darkened :table-hover-bgcolor, :table-bgcolor or :color3
+         :table-text-color nil ; defaults to bootstrap colors
+         :table-stripe-color nil ; background for every second table row, defaults to :color1
+
+         ;; Text
+         :header-border "3px solid #ccc"
+         :header-shadow nil ; defaults to :table-shadow
+         :link-color  "#025b96"
+         :link-hover-color nil ; defaults to color4
+         :font-family "'Lato', sans-serif"
+         ;; Text colors:
+         :text-primary nil
+         :bg-primary nil
+         :text-secondary nil
+         :bg-secondary nil
+         :text-success nil
+         :bg-success nil
+         :text-danger nil
+         :bg-danger nil
+         :text-warning "#ffc107!important"
+         :bg-warning nil
+         :text-info nil
+         :bg-info nil
+         :text-light nil
+         :bg-light nil
+         :text-dark nil
+         :bg-dark nil
+         :text-white nil
+         :bg-white nil
+         :text-muted nil
+
+         ;; Alerts
+         :alert-primary-color nil ; defaults to bootstrap colors
+         :alert-primary-bgcolor nil ; defaults to bootstrap colors
+         :alert-primary-bordercolor nil ; defaults to :alert-primary-color
+         :alert-secondary-color nil ; defaults to bootstrap colors
+         :alert-secondary-bgcolor nil ; defaults to bootstrap colors
+         :alert-secondary-bordercolor nil ; defaults to :alert-secondary-color
+         :alert-success-color nil ; defaults to bootstrap colors
+         :alert-success-bgcolor nil ; defaults to bootstrap colors
+         :alert-success-bordercolor nil ; defaults to :alert-success-color
+         :alert-danger-color nil ; defaults to bootstrap colors
+         :alert-danger-bgcolor nil ; defaults to bootstrap colors
+         :alert-danger-bordercolor nil ; defaults to :alert-danger-color
+         :alert-warning-color nil ; defaults to bootstrap colors
+         :alert-warning-bgcolor nil ; defaults to bootstrap colors
+         :alert-warning-bordercolor nil ; defaults to :alert-warning-color
+         :alert-info-color nil ; defaults to bootstrap colors
+         :alert-info-bgcolor nil ; defaults to bootstrap colors
+         :alert-info-bordercolor nil ; defaults to :alert-info-color
+         :alert-light-color nil ; defaults to bootstrap colors
+         :alert-light-bgcolor nil ; defaults to bootstrap colors
+         :alert-light-bordercolor nil ; defaults to :alert-light-color
+         :alert-dark-color nil ; defaults to bootstrap colors
+         :alert-dark-bgcolor nil ; defaults to bootstrap colors
+         :alert-dark-bordercolor nil ; defaults to :alert-dark-color
+
+         ;; Buttons
+         :primary-button-color "#fff"
+         :primary-button-bgcolor nil ; defaults to :color4
+         :primary-button-hover-color nil ; defaults to :primary-button-color
+         :primary-button-hover-bgcolor nil ; defaults to :primary-button-bgcolor
+         :secondary-button-color nil ; defaults to bootstrap colors
+         :secondary-button-bgcolor nil ; defaults to bootstrap colors
+         :secondary-button-hover-color nil ; defaults to bootstrap colors
+         :secondary-button-hover-bgcolor nil ; defaults to bootstrap colors
+
+         ;; Misc
+         :footer-color nil ; defaults to :table-heading-color
+         :footer-bgcolor nil ; defaults to :table-heading-bgcolor or color3
+         :collapse-shadow nil ; defaults to :table-shadow
+         :collapse-bgcolor "#fff"
+         :collapse-color "#fff"}
 
  ;; Optional extra static resources directory.
  :extra-static-resources nil
@@ -191,4 +300,3 @@
  :test-database-url nil
  :accessibility-report false ; should the axe accessibility tooling be included?
  :enable-ega false} ; should the EGA features be shown in the UI
-
diff --git a/src/clj/rems/css/style_utils.clj b/src/clj/rems/css/style_utils.clj
index 5cf0242e1..81630a031 100644
--- a/src/clj/rems/css/style_utils.clj
+++ b/src/clj/rems/css/style_utils.clj
@@ -2,13 +2,32 @@
   "The namespace contains the helpful style utils that are meant to be kept separate
    the main styles file to keep it cleaner."
   (:require [clojure.string :as str]
-            [garden.stylesheet :as stylesheet]
             [rems.config :refer [env]]))
 
 
-;; Customazable theme related functions
-
-(defn get-theme-attribute
+;; Customizable theme related functions
+
+(def ^:private ignore-theme-var-error?
+  ;; We don't want to include all these localized versions in
+  ;; config-defaults.edn. Luckily there are only a few so we can just
+  ;; have a whitelist.
+  #{:logo-name-fi
+    :logo-name-fi-sm
+    :navbar-logo-name-fi
+    :logo-name-sv
+    :logo-name-sv-sm
+    :navbar-logo-name-sv})
+
+(defn- theme-getx-impl
+  "Helper for making sure we document all our theme variables."
+  [attr]
+  (if-let [[_ v] (find (:theme env) attr)] ; find instead of get: nil values are ok, missing values are bad
+    v
+    (when (:dev env)
+      (when-not (ignore-theme-var-error? attr)
+        (assert false (str "Theme attribute " attr " used but not documented in config-defaults.edn!"))))))
+
+(defn theme-getx
   "Fetch the attribute value from the current theme with fallbacks.
 
   Keywords denote attribute lookups while strings are interpreted as fallback constant value."
@@ -16,7 +35,7 @@
   (when (seq attr-names)
     (let [attr-name (first attr-names)
           attr-value (if (keyword? attr-name)
-                       (get (:theme env) attr-name)
+                       (theme-getx-impl attr-name)
                        attr-name)]
       (or attr-value (recur (rest attr-names))))))
 
@@ -24,15 +43,14 @@
   (when path
     (let [url (if (str/starts-with? path "http")
                 path
-                (str (get-theme-attribute :img-path "../../img/") path))]
+                (str (theme-getx :img-path) path))]
       (str "url(\"" url "\")"))))
 
 (defn get-logo-image [lang]
-  (resolve-image (get-theme-attribute (keyword (str "logo-name-" (name lang))) :logo-name)))
+  (resolve-image (theme-getx (keyword (str "logo-name-" (name lang))) :logo-name)))
 
 (defn get-logo-name-sm [lang]
-  (resolve-image (get-theme-attribute (keyword (str "logo-name-" (name lang) "-sm")) :logo-name-sm)))
+  (resolve-image (theme-getx (keyword (str "logo-name-" (name lang) "-sm")) :logo-name-sm)))
 
 (defn get-navbar-logo [lang]
-  (resolve-image (get-theme-attribute (keyword (str "navbar-logo-name-" (name lang))) :navbar-logo-name)))
-
+  (resolve-image (theme-getx (keyword (str "navbar-logo-name-" (name lang))) :navbar-logo-name)))
diff --git a/src/clj/rems/css/styles.clj b/src/clj/rems/css/styles.clj
index 8d98d3840..f16ddd547 100644
--- a/src/clj/rems/css/styles.clj
+++ b/src/clj/rems/css/styles.clj
@@ -20,7 +20,7 @@
             [mount.core :as mount]
             [rems.config :refer [env]]
             [rems.context :as context]
-            [rems.css.style-utils :refer [get-theme-attribute get-logo-image get-navbar-logo get-logo-name-sm]]
+            [rems.css.style-utils :refer [theme-getx get-logo-image get-navbar-logo get-logo-name-sm]]
             [ring.util.response :as response]))
 
 (def content-width (u/px 1200))
@@ -84,12 +84,12 @@
 (defn- generate-logo-styles []
   (list
    [:.logo-menu {:height logo-height-menu
-                 :background-color (get-theme-attribute :logo-bgcolor)
+                 :background-color (theme-getx :logo-bgcolor)
                  :width "100px"
                  :padding-top "0px"
                  :padding-bottom "0px"}]
    [:.logo {:height logo-height
-            :background-color (get-theme-attribute :logo-bgcolor)
+            :background-color (theme-getx :logo-bgcolor)
             :width "100%"
             :margin "0 auto"
             :padding "0 20px"
@@ -97,20 +97,20 @@
    [(s/descendant :.logo :.img)
     (s/descendant :.logo-menu :.img)
     {:height "100%"
-     :background-color (get-theme-attribute :logo-bgcolor)
+     :background-color (theme-getx :logo-bgcolor)
      :-webkit-background-size :contain
      :-moz-o-background-size :contain
      :-o-background-size :contain
      :background-size :contain
      :background-repeat :no-repeat
      :background-position [[:center :center]]
-     :background-origin (get-theme-attribute :logo-content-origin)}]
+     :background-origin (theme-getx :logo-content-origin)}]
    [(s/descendant :.logo :.img) {:background-image (get-logo-image context/*lang*)}]
    [(s/descendant :.logo-menu :.img) {:background-image (get-navbar-logo context/*lang*)}]
    (stylesheet/at-media {:max-width (u/px 480)}
                         (list
                          [(s/descendant :.logo :.img)
-                          {:background-color (get-theme-attribute :logo-bgcolor)
+                          {:background-color (theme-getx :logo-bgcolor)
                            :background-image (get-logo-name-sm context/*lang*)
                            :-webkit-background-size :contain
                            :-moz-background-size :contain
@@ -128,8 +128,8 @@
              :flex-direction "row"
              :justify-content "stretch"
              :align-items "center"}
-   [:.phase {:background-color (get-theme-attribute :phase-bgcolor "#eee")
-             :color (get-theme-attribute :phase-color "#111")
+   [:.phase {:background-color (theme-getx :phase-bgcolor)
+             :color (theme-getx :phase-color)
              :flex-grow 1
              :height (u/px 40)
              :display "flex"
@@ -153,14 +153,12 @@
                                           :border-left [[(u/px 10) :solid :white]]
                                           :border-bottom [[(u/px 20) :solid :transparent]]
                                           :border-right "none"}]
-    [:&.active (merge {:color (get-theme-attribute :phase-color-active :phase-color "#111")}
-                      (if-let [background (get-theme-attribute :phase-background-active)]
-                        {:background background}
-                        {:background-color (get-theme-attribute :phase-bgcolor-active "#ccc")
-                         :border-color (get-theme-attribute :phase-bgcolor-active "#ccc")}))]
-    [:&.completed {:background-color (get-theme-attribute :phase-bgcolor-completed "#ccc")
-                   :border-color (get-theme-attribute :phase-bgcolor-completed "#ccc")
-                   :color (get-theme-attribute :phase-color-completed :phase-color)}]]])
+    [:&.active {:color (theme-getx :phase-color-active)
+                :background-color (theme-getx :phase-bgcolor-active)
+                :border-color (theme-getx :phase-bgcolor-active)}]
+    [:&.completed {:color (theme-getx :phase-color-completed)
+                   :background-color (theme-getx :phase-bgcolor-completed)
+                   :border-color (theme-getx :phase-bgcolor-completed)}]]])
 
 (defn- generate-actions-float-menu
   "The #actions floating menu can be too long for some screens. There is no clean solution for this in pure CSS
@@ -183,16 +181,17 @@
                                     :overflow-y :auto
                                     :max-height "calc(100vh - 105px)"}])))
 
+;; TODO inline me
 (defn- button-navbar-font-weight []
   ;; Default font-weight to 700 so the text is considered
   ;; 'large text' and thus requires smaller contrast ratio for
   ;; accessibility.
-  (get-theme-attribute :button-navbar-font-weight 700))
+  (theme-getx :button-navbar-font-weight))
 
 (defn table-selection-bgcolor []
-  (if-let [selection-bgcolor (get-theme-attribute :table-selection-bgcolor)]
+  (if-let [selection-bgcolor (theme-getx :table-selection-bgcolor)]
     selection-bgcolor
-    (-> (get-theme-attribute :table-hover-bgcolor :table-bgcolor :color3 "#777777")
+    (-> (theme-getx :table-hover-bgcolor :table-bgcolor :color3)
         (c/darken 15))))
 
 (defn- generate-rems-table-styles []
@@ -200,7 +199,7 @@
    [:.rems-table.cart {:background "#fff"
                        :color "#000"
                        :margin 0}
-    [".cart-bundle:not(:last-child)" {:border-bottom [[(u/px 1) :solid (get-theme-attribute :color1)]]}]
+    [".cart-bundle:not(:last-child)" {:border-bottom [[(u/px 1) :solid (theme-getx :color1)]]}]
     [:td:before {:content "initial"}]
     [:th
      :td:before
@@ -209,15 +208,15 @@
      [(s/& (s/nth-child "2n")) {:background "#fff"}]]]
    [:.table-border {:padding 0
                     :margin "1em 0"
-                    :border (get-theme-attribute :table-border "1px solid #ccc")
+                    :border (theme-getx :table-border)
                     :border-radius (u/rem 0.4)}]
    [:.rems-table {:min-width "100%"
-                  :background-color (get-theme-attribute :table-bgcolor :color1)
-                  :box-shadow (get-theme-attribute :table-shadow)
-                  :color (get-theme-attribute :table-text-color)}
+                  :background-color (theme-getx :table-bgcolor :color1)
+                  :box-shadow (theme-getx :table-shadow)
+                  :color (theme-getx :table-text-color)}
     [:th {:white-space "nowrap"
-          :color (get-theme-attribute :table-heading-color "#fff")
-          :background-color (get-theme-attribute :table-heading-bgcolor :color3)}]
+          :color (theme-getx :table-heading-color)
+          :background-color (theme-getx :table-heading-bgcolor :color3)}]
     [:th
      :td
      {:text-align "left"
@@ -225,21 +224,21 @@
     [:.selection {:width (u/rem 0.5)
                   :padding-right 0}]
     [:td:before
-     {:color (get-theme-attribute :table-text-color)}]
+     {:color (theme-getx :table-text-color)}]
     [:tr {:margin "0 1rem"}
-     [:&:hover {:color (get-theme-attribute :table-hover-color :table-text-color "#fff")
-                :background-color (get-theme-attribute :table-hover-bgcolor :color2)}]
-     [:&.selected {:background-color (get-theme-attribute :table-selection-bgcolor (table-selection-bgcolor))}]
+     [:&:hover {:color (theme-getx :table-hover-color :table-text-color)
+                :background-color (theme-getx :table-hover-bgcolor :color2)}]
+     [:&.selected {:background-color (theme-getx :table-selection-bgcolor (table-selection-bgcolor))}]
      [(s/& (s/nth-child "2n"))
-      [:&:hover {:color (get-theme-attribute :table-hover-color :table-text-color "#fff")
-                 :background-color (get-theme-attribute :table-hover-bgcolor :color2)}]
-      {:background-color (get-theme-attribute :table-stripe-color :table-bgcolor :color1)}
-      [:&.selected {:background-color (get-theme-attribute :table-selection-bgcolor (table-selection-bgcolor))}]]]
+      [:&:hover {:color (theme-getx :table-hover-color :table-text-color)
+                 :background-color (theme-getx :table-hover-bgcolor :color2)}]
+      {:background-color (theme-getx :table-stripe-color :table-bgcolor :color1)}
+      [:&.selected {:background-color (theme-getx :table-selection-bgcolor (table-selection-bgcolor))}]]]
     [:td.commands:last-child {:text-align "right"
                               :padding-right (u/rem 1)}]]
    [:.rems-table.cart {:box-shadow :none}]
    [:.inner-cart {:margin (u/em 1)}]
-   [:.outer-cart {:border [[(u/px 1) :solid (get-theme-attribute :color1)]]
+   [:.outer-cart {:border [[(u/px 1) :solid (theme-getx :color1)]]
                   :border-radius (u/rem 0.4)}]
    [:.cart-title {:margin-left (u/em 1)
                   :margin-right (u/em 1)
@@ -250,7 +249,7 @@
    ;; TODO: Change naming of :color3? It is used as text color here,
    ;;   which means that it should have a good contrast with light background.
    ;;   This could be made explicit by changing the name accordingly.
-   [:.text-highlight {:color (get-theme-attribute :color3)
+   [:.text-highlight {:color (theme-getx :color3)
                       :font-weight "bold"}]))
 
 (defn- generate-form-group []
@@ -359,14 +358,14 @@
    [:a
     :button
     {:cursor :pointer
-     :color (get-theme-attribute :link-color "#025b96")}
-    [:&:hover {:color (get-theme-attribute :link-hover-color :color4)}]]
+     :color (theme-getx :link-color)}
+    [:&:hover {:color (theme-getx :link-hover-color :color4)}]]
    [:.pointer {:cursor :pointer}
     [:label.form-check-label {:cursor :pointer}]]
    [:html {:position :relative
            :min-width (u/px 320)
            :height (u/percent 100)}]
-   [:body {:font-family (get-theme-attribute :font-family "'Lato', sans-serif")
+   [:body {:font-family (theme-getx :font-family)
            :min-height (u/percent 100)
            :display :flex
            :flex-direction :column
@@ -383,8 +382,8 @@
                       :display :flex
                       :flex-direction :column}]
    [:.fixed-top {:background-color "#fff"
-                 :border-bottom (get-theme-attribute :header-border "3px solid #ccc")
-                 :box-shadow (get-theme-attribute :header-shadow :table-shadow)
+                 :border-bottom (theme-getx :header-border)
+                 :box-shadow (theme-getx :header-shadow :table-shadow)
                  :min-height menu-height}]
    [:.skip-navigation {:position :absolute
                        :left (u/em -1000)}
@@ -415,13 +414,13 @@
     [:&:hover
      :&:focus
      :&:active:hover
-     {:background-color (get-theme-attribute :primary-button-hover-bgcolor :primary-button-bgcolor :color4)
-      :border-color (get-theme-attribute :primary-button-hover-bgcolor :primary-button-bgcolor :color4)
-      :color (get-theme-attribute :primary-button-hover-color :primary-button-color "#fff")
+     {:background-color (theme-getx :primary-button-hover-bgcolor :primary-button-bgcolor :color4)
+      :border-color (theme-getx :primary-button-hover-bgcolor :primary-button-bgcolor :color4)
+      :color (theme-getx :primary-button-hover-color :primary-button-color)
       :outline-color :transparent}]
-    {:background-color (get-theme-attribute :primary-button-bgcolor :color4)
-     :border-color (get-theme-attribute :primary-button-bgcolor :color4)
-     :color (get-theme-attribute :primary-button-color "#fff")
+    {:background-color (theme-getx :primary-button-bgcolor :color4)
+     :border-color (theme-getx :primary-button-bgcolor :color4)
+     :color (theme-getx :primary-button-color)
      :outline-color :transparent}]
    [:.btn-secondary
     ;; Only override bootstrap's default if the key is defined in the theme
@@ -430,15 +429,15 @@
      :&:active:hover
      (into {}
            (filter val
-                   {:background-color (get-theme-attribute :secondary-button-hover-bgcolor)
-                    :border-color (get-theme-attribute :secondary-button-hover-bgcolor)
-                    :color (get-theme-attribute :secondary-button-hover-color)
+                   {:background-color (theme-getx :secondary-button-hover-bgcolor)
+                    :border-color (theme-getx :secondary-button-hover-bgcolor)
+                    :color (theme-getx :secondary-button-hover-color)
                     :outline-color :transparent}))]
     (into {}
           (filter val
-                  {:background-color (get-theme-attribute :secondary-button-bgcolor)
-                   :border-color (get-theme-attribute :secondary-button-bgcolor)
-                   :color (get-theme-attribute :secondary-button-color)
+                  {:background-color (theme-getx :secondary-button-bgcolor)
+                   :border-color (theme-getx :secondary-button-bgcolor)
+                   :color (theme-getx :secondary-button-color)
                    :outline-color :transparent}))]
    [:.btn-primary.disabled :.btn-primary:disabled ; same color as bootstrap's default for .btn-secondary.disabled
     {:color "#fff"
@@ -455,64 +454,67 @@
                      :padding "0.25em"
                      :text-align :center
                      :color "#ccc"}
-                    [:&:hover {:color (get-theme-attribute :color4)
+                    [:&:hover {:color (theme-getx :color4)
                                :background-color "#eee"}]]]
    [:.flash-message-title {:font-weight :bold}]
 
-   [:.text-primary {:color (get-theme-attribute :text-primary)}]
-   [:.text-secondary {:color (get-theme-attribute :text-secondary)}]
-   [:.text-success {:color (get-theme-attribute :text-success)}]
-   [:.text-danger {:color (get-theme-attribute :text-danger)}]
-   [:.text-warning {:color (get-theme-attribute :text-warning "#ffc107!important")}]
-   [:.text-info {:color (get-theme-attribute :text-info)}]
-   [:.text-light {:color (get-theme-attribute :text-light)}]
-   [:.text-dark {:color (get-theme-attribute :text-dark)}]
-   [:.text-muted {:color (get-theme-attribute :text-muted)}]
-   [:.text-white {:color (get-theme-attribute :text-white)}]
-
-   [:.bg-primary {:background-color (get-theme-attribute :bg-primary)}]
-   [:.bg-secondary {:background-color (get-theme-attribute :bg-secondary)}]
-   [:.bg-success {:background-color (get-theme-attribute :bg-success)}]
-   [:.bg-danger {:background-color (get-theme-attribute :bg-danger)}]
-   [:.bg-warning {:background-color (get-theme-attribute :bg-warning)}]
-   [:.bg-info {:background-color (get-theme-attribute :bg-info)}]
-   [:.bg-light {:background-color (get-theme-attribute :bg-light)}]
-   [:.bg-dark {:background-color (get-theme-attribute :bg-dark)}]
-   [:.bg-white {:background-color (get-theme-attribute :bg-white)}]
-
-   [:.alert-primary {:color (get-theme-attribute :alert-primary-color)
-                     :background-color (get-theme-attribute :alert-primary-bgcolor)
-                     :border-color (get-theme-attribute :alert-primary-bordercolor :alert-primary-color)}]
-   [:.alert-secondary {:color (get-theme-attribute :alert-secondary-color)
-                       :background-color (get-theme-attribute :alert-secondary-bgcolor)
-                       :border-color (get-theme-attribute :alert-secondary-bordercolor :alert-secondary-color)}]
+   ;; TODO get rid of the text classes we don't use? At least -dark,
+   ;; -white, -light and -info seem unused currently.
+   [:.text-primary {:color (theme-getx :text-primary)}]
+   [:.text-secondary {:color (theme-getx :text-secondary)}]
+   [:.text-success {:color (theme-getx :text-success)}]
+   [:.text-danger {:color (theme-getx :text-danger)}]
+   [:.text-warning {:color (theme-getx :text-warning)}]
+   [:.text-info {:color (theme-getx :text-info)}]
+   [:.text-light {:color (theme-getx :text-light)}]
+   [:.text-dark {:color (theme-getx :text-dark)}]
+   [:.text-muted {:color (theme-getx :text-muted)}]
+   [:.text-white {:color (theme-getx :text-white)}]
+
+   [:.bg-primary {:background-color (theme-getx :bg-primary)}]
+   [:.bg-secondary {:background-color (theme-getx :bg-secondary)}]
+   [:.bg-success {:background-color (theme-getx :bg-success)}]
+   [:.bg-danger {:background-color (theme-getx :bg-danger)}]
+   [:.bg-warning {:background-color (theme-getx :bg-warning)}]
+   [:.bg-info {:background-color (theme-getx :bg-info)}]
+   [:.bg-light {:background-color (theme-getx :bg-light)}]
+   [:.bg-dark {:background-color (theme-getx :bg-dark)}]
+   [:.bg-white {:background-color (theme-getx :bg-white)}]
+
+   ;; TODO get rid of alert classes we don't use
+   [:.alert-primary {:color (theme-getx :alert-primary-color)
+                     :background-color (theme-getx :alert-primary-bgcolor)
+                     :border-color (theme-getx :alert-primary-bordercolor :alert-primary-color)}]
+   [:.alert-secondary {:color (theme-getx :alert-secondary-color)
+                       :background-color (theme-getx :alert-secondary-bgcolor)
+                       :border-color (theme-getx :alert-secondary-bordercolor :alert-secondary-color)}]
    [:.alert-success
     (s/descendant :.state-approved.phases :.phase.completed)
     (s/descendant :.state-submitted.phases :.phase.completed)
-    {:color (get-theme-attribute :alert-success-color)
-     :background-color (get-theme-attribute :alert-success-bgcolor)
-     :border-color (get-theme-attribute :alert-success-bordercolor :alert-success-color)}]
+    {:color (theme-getx :alert-success-color)
+     :background-color (theme-getx :alert-success-bgcolor)
+     :border-color (theme-getx :alert-success-bordercolor :alert-success-color)}]
    [:.alert-danger
     :.state-rejected
     :.state-revoked
     (s/descendant :.state-rejected.phases :.phase.completed)
     (s/descendant :.state-revoked.phases :.phase.completed)
-    {:color (get-theme-attribute :alert-danger-color)
-     :background-color (get-theme-attribute :alert-danger-bgcolor)
-     :border-color (get-theme-attribute :alert-danger-bordercolor :alert-danger-color)}]
-   [:.alert-warning {:color (get-theme-attribute :alert-warning-color)
-                     :background-color (get-theme-attribute :alert-warning-bgcolor)
-                     :border-color (get-theme-attribute :alert-warning-bordercolor :alert-warning-color)}]
+    {:color (theme-getx :alert-danger-color)
+     :background-color (theme-getx :alert-danger-bgcolor)
+     :border-color (theme-getx :alert-danger-bordercolor :alert-danger-color)}]
+   [:.alert-warning {:color (theme-getx :alert-warning-color)
+                     :background-color (theme-getx :alert-warning-bgcolor)
+                     :border-color (theme-getx :alert-warning-bordercolor :alert-warning-color)}]
    [:.alert-info
-    {:color (get-theme-attribute :alert-info-color)
-     :background-color (get-theme-attribute :alert-info-bgcolor)
-     :border-color (get-theme-attribute :alert-info-bordercolor :alert-info-color)}]
-   [:.alert-light {:color (get-theme-attribute :alert-light-color)
-                   :background-color (get-theme-attribute :alert-light-bgcolor)
-                   :border-color (get-theme-attribute :alert-light-bordercolor :alert-light-color)}]
-   [:.alert-dark {:color (get-theme-attribute :alert-dark-color)
-                  :background-color (get-theme-attribute :alert-dark-bgcolor)
-                  :border-color (get-theme-attribute :alert-dark-bordercolor :alert-dark-color)}]
+    {:color (theme-getx :alert-info-color)
+     :background-color (theme-getx :alert-info-bgcolor)
+     :border-color (theme-getx :alert-info-bordercolor :alert-info-color)}]
+   [:.alert-light {:color (theme-getx :alert-light-color)
+                   :background-color (theme-getx :alert-light-bgcolor)
+                   :border-color (theme-getx :alert-light-bordercolor :alert-light-color)}]
+   [:.alert-dark {:color (theme-getx :alert-dark-color)
+                  :background-color (theme-getx :alert-dark-bgcolor)
+                  :border-color (theme-getx :alert-dark-bordercolor :alert-dark-color)}]
    shake
    [:.flash-message.alert-danger
     {:animation [[shake "0.6s cubic-bezier(.36,.07,.19,.97) both"]]}]
@@ -539,7 +541,7 @@
      :letter-spacing (u/rem 0.015)
      :padding-left 0
      :padding-right 0
-     :color (get-theme-attribute :navbar-color "#111")
+     :color (theme-getx :navbar-color)
      :justify-content "space-between"}
     [:.nav-link :.btn-link
      {:background-color :inherit}]]
@@ -551,25 +553,25 @@
                       :display :flex
                       :flex-direction :row}]
    [:.navbar-top-left {:flex 1
-                       :background-color (get-theme-attribute :color4)}]
+                       :background-color (theme-getx :color4)}]
    [:.navbar-top-right {:flex 1
-                        :background-color (get-theme-attribute :color2)}]
+                        :background-color (theme-getx :color2)}]
    [:.navbar-text {:font-size (u/px 19)
                    :font-weight (button-navbar-font-weight)}]
-   [:.navbar-toggler {:border-color (get-theme-attribute :color1)}]
+   [:.navbar-toggler {:border-color (theme-getx :color1)}]
    [:.nav-link
     :.btn-link
-    {:color (get-theme-attribute :nav-color :link-color :color3)
+    {:color (theme-getx :nav-color :link-color)
      :font-weight (button-navbar-font-weight)
      :border 0} ; for button links
     [:&.active
-     {:color (get-theme-attribute :nav-active-color :color4)}]
+     {:color (theme-getx :nav-active-color :color4)}]
     [:&:hover
-     {:color (get-theme-attribute :nav-hover-color :color4)}]]
+     {:color (theme-getx :nav-hover-color :color4)}]]
    [:.navbar {:white-space "nowrap"}]
    [(s/descendant :.user-widget :.nav-link) {:display :inline-block}]
    [:.user-name {:text-transform :none}]
-   [:#big-navbar {:text-transform (get-theme-attribute :big-navbar-text-transform "none")}]
+   [:#big-navbar {:text-transform (theme-getx :big-navbar-text-transform)}]
    [(s/descendant :.navbar-text :.language-switcher)
     {:margin-right (u/rem 1)}]
    [:.navbar-flex {:display "flex"
@@ -580,14 +582,14 @@
    ;; Logo, login, etc.
    (generate-logo-styles)
    ;; Footer
-   (let [footer-text-color (get-theme-attribute :footer-color :table-heading-color "#fff")]
+   (let [footer-text-color (theme-getx :footer-color :table-heading-color)]
      [:footer {:width "100%"
                :min-height (u/px 53.6)
                :color footer-text-color
                :font-size (u/px 19) ;; same as navbar
                :padding-top "1rem"
                :padding-bottom "1rem"
-               :background-color (get-theme-attribute :footer-bgcolor :table-heading-bgcolor :color3)
+               :background-color (theme-getx :footer-bgcolor :table-heading-bgcolor :color3)
                :margin-top (u/em 1)}
       [:a :a:hover {:color footer-text-color
                     :font-weight (button-navbar-font-weight)}]
@@ -602,7 +604,7 @@
      :margin-top (u/rem 2)
      :border-style "solid"
      :border-width (u/px 1)
-     :box-shadow (get-theme-attribute :collapse-shadow :table-shadow)}
+     :box-shadow (theme-getx :collapse-shadow :table-shadow)}
     [:h1 {:margin-bottom (u/px 20)}]]
    [:.login-btn {:max-height (u/px 70)
                  :margin-bottom (u/px 20)}
@@ -735,10 +737,10 @@
    [:.intro {:margin-bottom (u/rem 2)}]
    [:.rectangle {:width (u/px 50)
                  :height (u/px 50)}]
-   [:.color-1 {:background-color (get-theme-attribute :color1)}]
-   [:.color-2 {:background-color (get-theme-attribute :color2)}]
-   [:.color-3 {:background-color (get-theme-attribute :color3)}]
-   [:.color-4 {:background-color (get-theme-attribute :color4)}]
+   [:.color-1 {:background-color (theme-getx :color1)}]
+   [:.color-2 {:background-color (theme-getx :color2)}]
+   [:.color-3 {:background-color (theme-getx :color3)}]
+   [:.color-4 {:background-color (theme-getx :color4)}]
    [:.color-title {:padding-top (u/rem 0.8)}]
    [(s/descendant :.alert :ul) {:margin-bottom 0}]
    [:ul.comments {:list-style-type :none}]
@@ -749,8 +751,8 @@
                      :width "inherit"}]
    [:.clickable {:cursor :pointer}]
    [:.rems-card-margin-fix {:margin (u/px -1)}] ; make sure header overlaps container border
-   [:.rems-card-header {:color (get-theme-attribute :table-heading-color "#fff")
-                        :background-color (get-theme-attribute :table-heading-bgcolor :color3)}]
+   [:.rems-card-header {:color (theme-getx :table-heading-color)
+                        :background-color (theme-getx :table-heading-bgcolor :color3)}]
    [(s/descendant :.card-header :a) {:color :inherit}]
    [:.application-resources
     [:.application-resource {:margin-bottom (u/rem 1)
@@ -769,25 +771,24 @@
    [:.collapse-toggle {:text-align :center}]
    [:.collapse-wrapper {:border-radius (u/rem 0.4)
                         :border "1px solid #ccc"
-                        :background-color (get-theme-attribute :collapse-bgcolor "#fff")
-                        :box-shadow (get-theme-attribute :collapse-shadow :table-shadow)}
+                        :background-color (theme-getx :collapse-bgcolor)
+                        :box-shadow (theme-getx :collapse-shadow :table-shadow)}
     [:.card-header {:border-bottom "none"
                     :border-radius (u/rem 0.4)
                     :font-weight 400
                     :font-size (u/rem 1.5)
                     :line-height 1.1
-                    :font-family (get-theme-attribute :font-family "'Lato', sans-serif")
-                    :color (get-theme-attribute :collapse-color "#fff")}
-     [:&.alert-danger {:color (get-theme-attribute :danger-color "inherit")}]]]
+                    :font-family (theme-getx :font-family)
+                    :color (theme-getx :collapse-color)}]]
    [:.collapse-content {:margin (u/rem 1.25)}]
    [:.collapse-wrapper.slow
     [:.collapsing {:-webkit-transition "height 0.25s linear"
                    :-o-transition "height 0.25s linear"
                    :transition "height 0.25s linear"}]]
 
-   [:.color1 {:color (get-theme-attribute :color1)}]
-   [:.color1-faint {:color (when (get-theme-attribute :color1)
-                             (-> (get-theme-attribute :color1)
+   [:.color1 {:color (theme-getx :color1)}]
+   [:.color1-faint {:color (when (theme-getx :color1)
+                             (-> (theme-getx :color1)
                                  (c/saturate -50)
                                  (c/lighten 33)))}]
    [:h2 {:margin [[(u/rem 3) 0 (u/rem 1) 0]]}]
@@ -829,8 +830,8 @@
    ;; by more specific styles by react-select.
    [:.dropdown-select__option--is-focused
     (make-important
-     {:color (get-theme-attribute :table-heading-color "#fff")
-      :background-color (get-theme-attribute :table-heading-bgcolor :color3)})]
+     {:color (theme-getx :table-heading-color)
+      :background-color (theme-getx :table-heading-bgcolor :color3)})]
    [:.dropdown-select__control--is-focused
     (make-important
      {:color "#495057"
@@ -887,10 +888,6 @@
 (defn screen-css []
   (g/css {:pretty-print? false} (remove-nil-vals (build-screen))))
 
-(deftest test-screen-css
-  (binding [context/*lang* :fi]
-    (is (string? (screen-css)))))
-
 ;; For development use and Figwheel updates render all configured CSS
 ;; files so that Figwheel will notice this change and force our app
 ;; to reload CSS files from the usual route.


Test patch:
diff --git a/src/clj/rems/test_styles.clj b/src/clj/rems/test_styles.clj
new file mode 100644
index 000000000..9b8afc078
--- /dev/null
+++ b/src/clj/rems/test_styles.clj
@@ -0,0 +1,18 @@
+(ns ^:integration rems.test-styles
+  (:require [clojure.test :refer :all]
+            [mount.core :as mount]
+            [rems.config]
+            [rems.context :as context]
+            [rems.css.styles :as styles]))
+
+
+(use-fixtures
+  :once
+  (fn [f]
+    (mount/start #'rems.config/env)
+    (f)
+    (mount/stop)))
+
+(deftest screen-css-smoke-test
+  (binding [context/*lang* :fi]
+    (is (string? (styles/screen-css)))))
diff --git a/test/clj/rems/test_themes.clj b/test/clj/rems/test_themes.clj
index da2489019..3f844a640 100644
--- a/test/clj/rems/test_themes.clj
+++ b/test/clj/rems/test_themes.clj
@@ -32,9 +32,9 @@
       (is (= "example-theme/public"
              (:theme-static-resources (config/load-external-theme config)))))))
 
-(deftest get-theme-attribute-test
+(deftest theme-getx-test
   (with-redefs [config/env {:theme {:test "success"
                                     :test-color 2}}]
-    (is (= 2 (style-utils/get-theme-attribute :test-color)))
-    (is (= "success" (style-utils/get-theme-attribute :test)))
-    (is (nil? (style-utils/get-theme-attribute :no-such-attribute)))))
+    (is (= 2 (style-utils/theme-getx :test-color)))
+    (is (= "success" (style-utils/theme-getx :test)))
+    (is (nil? (style-utils/theme-getx :no-such-attribute)))))