Liking cljdoc? Tell your friends :D

Workflow Guide

Use these workflows for design systems, data visualization, accessibility, and lighting interfaces.

Table of Contents

Color Blending Modes

Blending modes combine two colors with the same rules designers use in tools like Photoshop or Figma.

Multiply

Multiply darkens colors by multiplying the color channels. Use it for shadows and dark overlays.

(require '[jon.color-tools :as color])

;; Multiply red and blue creates black
(color/blend-multiply "#ff0000" "#0000ff")
;=> "#000000"

;; Multiply with semi-bright colors
(color/blend-multiply "#ff8800" "#88ff00")
;=> "#888800"

;; RGB vectors work too
(color/blend-multiply [255 128 64] [128 255 200])
;=> [128 128 50]

Use cases:

  • Shadows
  • Dark image or background overlays
  • Layer compositing in design tools

Screen

Screen lightens colors by inverting, multiplying, and inverting again.

;; Screen brightens dark colors
(color/blend-screen "#800000" "#000080")
;=> "#800080"

;; Screen with bright colors creates very bright results
(color/blend-screen "#ff8800" "#88ff00")
;=> "#ffff00"

Use cases:

  • Highlights
  • Lightening images
  • Glow and light treatments

Overlay

Overlay combines multiply and screen based on base brightness. Dark areas get darker; light areas get lighter.

;; Overlay creates high contrast
(color/blend-overlay "#ff0000" "#808080")
;=> "#ff0000"

;; Texture overlay
(color/blend-overlay "#3498db" "#f0f0f0")
;=> "#62f3fb"

Use cases:

  • Surface textures
  • Contrast adjustments
  • High-contrast color effects

Preserving Input Format

Blending functions preserve the base color's input format:

;; Hex input returns hex output
(color/blend-multiply "#ff0000" "#0000ff")
;=> "#000000"

;; Vector input returns vector output
(color/blend-multiply [255 0 0] [0 0 255])
;=> [0 0 0]

;; Color record input returns Color record output
(def red-color (color/color 255 0 0))
(def blue-color (color/color 0 0 255))
(color/blend-multiply red-color blue-color)
;=> #Color{:r 0 :g 0 :b 0 :a 1.0}

Tints, Shades, and Tones

Tints, shades, and tones create related colors from one base color.

Understanding the Difference

  • Tint: Color + White (lightens while preserving hue)
  • Shade: Color + Black (darkens while preserving hue)
  • Tone: Color + Gray (reduces saturation/vibrancy)

Creating Single Variations

(def base-color "#3498db")  ; A nice blue

;; Create a tint (lighter version)
(color/tint base-color 0.3)
;=> "#71b7e6"

;; Create a shade (darker version)
(color/shade base-color 0.3)
;=> "#246a99"

;; Create a tone (less saturated version)
(color/tone base-color 0.3)
;=> "#4b91c0"

The amount parameter (0-1) controls how much white/black/gray to mix:

  • 0.0 = no change
  • 0.5 = 50% mixed
  • 1.0 = pure white/black/gray

Generating Color Scales

Use tint, shade, and tone series for design system scales:

;; Generate 5 lighter tints
(color/tints "#e74c3c" 5)
;=> ["#ec7063" "#f1948a" "#f5b7b1" "#fadbd8" "#ffffff"]

;; Generate 5 darker shades
(color/shades "#e74c3c" 5)
;=> ["#b93d30" "#8b2e24" "#5c1e18" "#2e0f0c" "#000000"]

;; Generate 5 less saturated tones
(color/tones "#e74c3c" 5)
;=> ["#d2564a" "#be6157" "#a96b65" "#957672" "#808080"]

Building Design Systems

;; Create a brand palette
(def brand-primary "#2ecc71")

(def palette
  {:primary brand-primary
   :primary-light (color/tint brand-primary 0.2)
   :primary-lighter (color/tint brand-primary 0.4)
   :primary-dark (color/shade brand-primary 0.2)
   :primary-darker (color/shade brand-primary 0.4)
   :primary-muted (color/tone brand-primary 0.3)})

palette
;=> {:primary "#2ecc71"
;    :primary-light "#58d68d"
;    :primary-lighter "#82e0a9"
;    :primary-dark "#25a25a"
;    :primary-darker "#1c7943"
;    :primary-muted "#4db67a"}

Alpha Blending and Compositing

Alpha blending controls how transparent colors stack.

Porter-Duff Alpha Compositing

alpha-blend implements Porter-Duff "source over" compositing:

;; Blend semi-transparent blue over opaque red
(color/alpha-blend [255 0 0 1.0] [0 0 255 0.5])
;=> [128 0 128 1.0]

;; The formula: 
;; a_out = a_overlay + a_base * (1 - a_overlay)
;; c_out = (c_overlay * a_overlay + c_base * a_base * (1 - a_overlay)) / a_out

Setting Alpha Channels

;; Add transparency to a color
(color/with-alpha "#ff0000" 0.5)
;=> "rgba(255,0,0,0.5)"

;; Works with all color types
(color/with-alpha [255 0 0] 0.7)
;=> [255 0 0 0.7]

(color/with-alpha (color/color 255 0 0) 0.3)
;=> Color record with alpha 0.3

Layering Colors

;; Simulate layered UI elements
(def background "#ffffff")
(def overlay1 (color/with-alpha "#3498db" 0.3))
(def overlay2 (color/with-alpha "#e74c3c" 0.3))

;; Layer overlay1 on background
(def layer1 (color/alpha-blend background overlay1))

;; Then layer overlay2 on top
(def final (color/alpha-blend layer1 overlay2))

Practical Example: Glass Morphism

(defn create-glass-effect [background-color glass-tint alpha blur]
  (let [tinted (color/tint background-color glass-tint)
        with-transparency (color/with-alpha tinted alpha)]
    {:background with-transparency
     :backdrop-filter (str "blur(" blur "px)")
     :border "1px solid rgba(255,255,255,0.18)"}))

(create-glass-effect "#1a1a1a" 0.1 0.15 10)
;=> {:background "rgba(37,37,37,0.15)"
;    :backdrop-filter "blur(10px)"
;    :border "1px solid rgba(255,255,255,0.18)"}

Color Interpolation and Gradients

Create smooth color transitions for animations, data visualizations, and UI effects.

Basic Interpolation

;; Find the color halfway between red and blue
(color/interpolate ["#ff0000" "#0000ff"] 0.5 :rgb)
;=> "#800080"  ; Purple

;; Position can be any value from 0 to 1
(color/interpolate ["#ff0000" "#0000ff"] 0.25 :rgb)
;=> "#bf0040"  ; More red than blue

(color/interpolate ["#ff0000" "#0000ff"] 0.75 :rgb)
;=> "#4000bf"  ; More blue than red

Multi-Color Interpolation

;; Interpolate through multiple colors
(def colors ["#ff0000" "#00ff00" "#0000ff"])

(color/interpolate colors 0.0 :rgb)   ; Start
;=> "#ff0000"

(color/interpolate colors 0.5 :rgb)   ; Middle (at green)
;=> "#00ff00"

(color/interpolate colors 1.0 :rgb)   ; End
;=> "#0000ff"

Color Space Matters

Different color spaces produce different results:

(def red "#ff0000")
(def yellow "#ffff00")

;; RGB interpolation (direct component mixing)
(color/interpolate [red yellow] 0.5 :rgb)
;=> "#ff8000"  ; Orange (halfway in RGB space)

;; HSL interpolation (through hue wheel)
(color/interpolate [red yellow] 0.5 :hsl)
;=> "#ff8000"  ; Orange through hue 0 to 60

;; For colors across the hue wheel, HSL is more natural
(def red "#ff0000")
(def cyan "#00ffff")

(color/interpolate [red cyan] 0.5 :rgb)
;=> "#808080"  ; Gray (RGB mixes to gray)

(color/interpolate [red cyan] 0.5 :hsl)
;=> "#80ff00"  ; Chartreuse: hue 90°, halfway from red to cyan

Creating Gradients

;; Simple 2-color gradient
(color/gradient ["#ff0000" "#0000ff"] 5 :rgb)
;=> ["#ff0000" "#bf0040" "#800080" "#4000bf" "#0000ff"]

;; Multi-color gradient
(color/gradient ["#ff0000" "#00ff00" "#0000ff"] 7 :hsl)
;=> ["#ff0000" "#ffaa00" "#aaff00" "#00ff00" "#00ffaa" "#00aaff" "#0000ff"]

;; Fine-grained gradient for smooth transitions
(color/gradient ["#3498db" "#e74c3c"] 20 :rgb)
;=> Twenty colors from blue to red

Practical Examples

Heat Map Colors:

(def heat-map-gradient
  (color/gradient ["#0000ff" "#00ffff" "#00ff00" "#ffff00" "#ff0000"] 100 :rgb))

(defn value->color [value min-val max-val]
  (let [normalized (/ (- value min-val) (- max-val min-val))
        index (int (* normalized 99))]
    (nth heat-map-gradient index)))

(value->color 50 0 100)  ; Mid-range value
;=> "#00ff05"  ; Green

Animated Color Transitions:

(defn animate-color-transition [start-color end-color duration-ms current-time-ms]
  (let [progress (/ current-time-ms duration-ms)
        clamped (max 0 (min 1 progress))]
    (color/interpolate [start-color end-color] clamped :hsl)))

(animate-color-transition "#3498db" "#e74c3c" 1000 500)
;=> Color at 50% of animation

Perceptual Color Difference

RGB distance does not match human color perception. CIE Delta E measures perceived difference in LAB color space.

Delta E

;; These have similar RGB distances
(color/color-distance "#ff0000" "#ff0100")
;=> 1.0

(color/color-distance "#00ff00" "#00ff01")
;=> 1.0

;; Delta E separates these cases.
(color/delta-e "#ff0000" "#ff0100")
;=> 0.07  ; Imperceptible

(color/delta-e "#00ff00" "#00ff01")
;=> 0.08  ; Also imperceptible

Understanding Delta E Values

  • < 1.0: Imperceptible difference (colors appear identical)
  • 1.0 - 2.0: Barely perceptible (experts might notice)
  • 2.0 - 10.0: Noticeable difference (most people can see it)
  • > 10.0: Very different colors

Checking Similarity

;; Using default threshold (2.3 JND - Just Noticeable Difference)
(color/perceptually-similar? "#ff0000" "#fe0101")
;=> true

(color/perceptually-similar? "#ff0000" "#0000ff")
;=> false

;; Custom threshold for stricter matching
(color/perceptually-similar? "#ff0000" "#fe0000" 1.0)
;=> true   ; Delta E 0.37 is under the stricter 1.0

LAB Color Space

Delta E uses the CIE L*a*b* color space:

;; Convert to LAB
(color/rgb->lab [255 0 0])
;=> [53.23 80.11 67.2]
;; L: lightness (0-100)
;; a: green-red axis
;; b: blue-yellow axis

(color/rgb->lab [0 255 0])
;=> [87.74 -86.18 83.18]
;; Negative a = more green

Oklab Lightness

Use rgb->oklab to work with Oklab coordinates. Pass an sRGB vector with channels from 0 to 255, or convert another input format with ->rgb. The first output coordinate measures lightness on a 0 to 1 scale.

Sort swatches from dark to light:

(sort-by (comp first color/rgb->oklab color/->rgb)
         ["#ffffff" "#336699" "#000000"])
;=> ("#000000" "#336699" "#ffffff")

For the other axes and numeric examples, see rgb->oklab in the API reference. Use delta-e for CIE76 differences in CIE Lab*; adding Oklab conversion does not change that calculation.

Lightness Ramps in Oklab

Oklab lightness steps look even, so you can build a ramp by changing l and keeping a and b. Convert each step back with oklab->hex:

(let [[_ a b] (color/rgb->oklab (color/->rgb "#336699"))]
  (mapv #(color/oklab->hex [% a b]) [0.35 0.45 0.55 0.65 0.75]))
;=> ["#023c6b" "#24588a" "#4275a9" "#5f93c9" "#7db3ea"]

Lighter steps eventually leave the sRGB gamut, and oklab->hex clamps them. Check each step with in-gamut? to catch that:

(let [[_ a b] (color/rgb->oklab (color/->rgb "#336699"))]
  (color/in-gamut? [0.85 a b]))
;=> false

Hue and Chroma in OKLCh

OKLCh splits Oklab into lightness, chroma and hue. Rotating the hue keeps lightness and chroma, so every color in the palette has the same perceived lightness:

(let [[l c h] (color/rgb->oklch (color/->rgb "#336699"))]
  (mapv #(color/oklch->hex [l c (+ h %)]) [0 120 240]))
;=> ["#336699" "#924956" "#4f6e2e"]

Changing only the chroma moves a color from gray to fully saturated at the same lightness and hue:

(let [[l _ h] (color/rgb->oklch (color/->rgb "#336699"))]
  (mapv #(color/oklch->hex [l % h]) [0 0.05 0.1 0.15]))
;=> ["#636363" "#4d657f" "#32669a" "#0064b4"]

High chroma leaves the sRGB gamut quickly. Check with (color/in-gamut? (color/oklch->oklab lch)) before relying on the hex.

Practical Applications

Color Palette Deduplication:

(defn deduplicate-colors [colors threshold]
  (reduce (fn [acc color]
            (if (some #(color/perceptually-similar? color % threshold) acc)
              acc
              (conj acc color)))
          []
          colors))

(deduplicate-colors ["#ff0000" "#fe0101" "#ff0100" "#0000ff"] 2.0)
;=> ["#ff0000" "#0000ff"]  ; Similar reds merged

Finding Closest Match:

(defn find-closest-color [target palette]
  (apply min-key
         #(color/delta-e target %)
         palette))

(def brand-colors ["#3498db" "#e74c3c" "#2ecc71" "#f39c12"])
(find-closest-color "#3a9edb" brand-colors)
;=> "#3498db"  ; Closest brand color

Color Temperature (Kelvin)

Color temperature represents the color of light emitted by a black body at a given temperature, measured in Kelvin.

Common Color Temperatures

  • 1800K: Candlelight (warm orange)
  • 2700K: Incandescent bulb (warm white)
  • 3000K: Warm white LED
  • 4000K: Neutral white
  • 5000K: Cool white
  • 6500K: Daylight (bluish white)
  • 10000K: Clear blue sky (deep blue)

Converting Temperature to Color

;; Candlelight
(color/kelvin->rgb 1800)
;=> [255 126 0]

(color/kelvin->hex 1800)
;=> "#ff7e00"

;; Warm white
(color/kelvin->rgb 3000)
;=> [255 177 110]

;; Daylight
(color/kelvin->rgb 6500)
;=> [255 254 250]

;; Cool blue
(color/kelvin->rgb 10000)
;=> [202 218 255]

Approximating Temperature from Color

;; Works best for colors on the black body curve
(color/rgb->kelvin (color/kelvin->rgb 1800))
;=> 1805  ; Close to 1800K

(color/rgb->kelvin [255 249 253])
;=> 6452  ; Close to 6500K

;; Returns nil for non-black-body colors
(color/rgb->kelvin [255 0 0])
;=> nil  ; Pure red is outside the curve

Valid Range

The temperature functions accept values from 1000K to 40000K:

(color/kelvin->rgb 1000)   ; OK
(color/kelvin->rgb 40000)  ; OK

(color/kelvin->rgb 500)    ; Exception: out of range
(color/kelvin->rgb 50000)  ; Exception: out of range

Practical Applications

Adjustable Lighting UI:

(defn create-light-controls [kelvin]
  {:color (color/kelvin->hex kelvin)
   :label (cond
            (< kelvin 2500) "Warm"
            (< kelvin 4000) "Neutral Warm"
            (< kelvin 5500) "Neutral"
            (< kelvin 7000) "Cool"
            :else "Very Cool")
   :slider-value kelvin})

(create-light-controls 3000)
;=> {:color "#ffd6aa"
;    :label "Neutral Warm"
;    :slider-value 3000}

Time-of-Day Lighting:

(defn time-based-temperature [hour]
  (cond
    (< hour 6)   1800   ; Night - candlelight
    (< hour 9)   3000   ; Early morning - warm
    (< hour 17)  5500   ; Day - neutral/cool
    (< hour 20)  3500   ; Evening - warm
    :else        2200)) ; Night - very warm

(defn get-ambient-color [hour]
  (color/kelvin->hex (time-based-temperature hour)))

(get-ambient-color 14)  ; 2 PM
;=> "#ffedde"  ; Bright daylight color

Mood Lighting:

(def mood-presets
  {:romantic   (color/kelvin->hex 1800)  ; Candlelight
   :relaxing   (color/kelvin->hex 2700)  ; Warm cozy
   :productive (color/kelvin->hex 5000)  ; Cool white
   :energizing (color/kelvin->hex 6500)}) ; Daylight

mood-presets
;=> {:romantic "#ff9329"
;    :relaxing "#ffc58f"
;    :productive "#ffeef0"
;    :energizing "#fff9fd"}

Accessible Theme Generation

accessible-theme turns one brand color into production UI tokens. It is designed for app themes where color roles need to work together: surfaces, text, primary actions, secondary actions, accents, borders, focus rings, and a numeric scale.

Creating a Light Theme

(def theme
  (color/accessible-theme "#1f86c7" {:mode :light :level :aa}))

(select-keys theme [:background :surface :primary :on-primary :focus-ring])
;=> {:background "#f6fafd"
;    :surface "#e9f3f9"
;    :primary "#1f86c7"
;    :on-primary "#000000"
;    :focus-ring "#6d1fc7"}

The :on-* tokens are foreground colors selected to meet the requested WCAG level against their matching background token.

(color/accessible? (:primary theme) (:on-primary theme) (:level theme))
;=> true

(color/accessible? (:surface theme) (:on-surface theme) (:level theme))
;=> true

Dark Mode and Stricter Contrast

Use :mode :dark to derive dark surfaces from the same brand color. Use :level :aaa when normal body text needs the stricter AAA threshold.

(def dark-theme
  (color/accessible-theme "#ff7a59" {:mode :dark :level :aaa}))

(select-keys dark-theme [:background :surface :primary :on-primary])
;=> {:background "#1f0f0b"
;    :surface "#381b14"
;    :primary "#ff7a59"
;    :on-primary "#000000"}

Exporting CSS Variables

theme->css-vars converts theme maps into CSS custom properties. Use it to apply Clojure/ClojureScript token generation in browser CSS.

(color/theme->css-vars theme {:prefix "--brand-"})
;=> {"--brand-background" "#f6fafd"
;    "--brand-on-background" "#111827"
;    "--brand-primary" "#1f86c7"
;    "--brand-on-primary" "#000000"
;    "--brand-scale-50" "#f4f9fc"
;    "--brand-scale-500" "#1f86c7"
;    ...}

Applying Tokens in ClojureScript

(defn theme-style [brand mode]
  (color/theme->css-vars
   (color/accessible-theme brand {:mode mode :level :aa})))

(defn app-shell []
  [:main {:style (theme-style "#1f86c7" :light)}
   [:button {:style {:background "var(--ct-primary)"
                     :color "var(--ct-on-primary)"
                     :outline-color "var(--ct-focus-ring)"}}
    "Save"]])

Palettes vs Themes

Use palette functions such as analogous, triadic, and monochromatic when you need color ideas or data visualization ranges. Use accessible-theme when you need app-ready roles with contrast-aware foreground colors and CSS export.

Conclusion

These workflows show how the color-tools API fits into design systems, data visualization, lighting applications, and frontend UI work.

For complete API documentation, see api-reference.md.

Can you improve this documentation?Edit on GitHub

cljdoc builds & hosts documentation for Clojure/Script libraries

Keyboard shortcuts
Ctrl+kJump to recent docs
←Move to previous article
→Move to next article
Ctrl+/Jump to the search field
× close