Use these workflows for design systems, data visualization, accessibility, and lighting interfaces.
Blending modes combine two colors with the same rules designers use in tools like Photoshop or Figma.
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:
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:
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:
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 create related colors from one base color.
(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:
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"]
;; 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 controls how transparent colors stack.
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
;; 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
;; 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))
(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)"}
Create smooth color transitions for animations, data visualizations, and UI effects.
;; 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
;; 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"
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
;; 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
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
RGB distance does not match human color perception. CIE Delta E measures perceived difference in LAB color space.
;; 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
;; 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
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
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.
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
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.
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 represents the color of light emitted by a black body at a given temperature, measured in Kelvin.
;; 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]
;; 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
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
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 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.
(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
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"}
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"
; ...}
(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"]])
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.
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
| Ctrl+k | Jump to recent docs |
| ← | Move to previous article |
| → | Move to next article |
| Ctrl+/ | Jump to the search field |