A color toolkit for Clojure and ClojureScript. Convert formats, manipulate colors, generate palettes, check contrast, and build accessible theme tokens.
Try the color picker, format converter, gradient generator, WCAG contrast checker, and color harmonies.
jon/color-tools gives Clojure apps one namespace for common color work:
format conversion, color adjustment, palettes, contrast checks, gradients, and
theme tokens.
Run the demo from the demo directory:
Options:
Prerequisites
Quick local options
# from the repository root
cd demo
npm install
npx shadow-cljs watch app
# open http://localhost:8080
demo/public):# from the repository root
cd demo
npm install
npx shadow-cljs release app
# then serve the generated files:
python3 -m http.server --directory demo/public 8000
# or
npx http-server demo/public -p 8000
# open http://localhost:8000
Add the following dependency to your deps.edn:
{:deps com.github.jramosg/color-tools {:mvn/version "1.3.0"}}
Or for Leiningen, add to your project.clj:
[com.github.jramosg/color-tools "1.3.0"]
(require '[jon.color-tools :as color])
;; Convert between formats - universal conversion functions
(color/->rgb "#ff5733") ; hex to RGB
;=> [255 87 51]
(color/->rgb [255 87 51]) ; RGB vector (passthrough)
;=> [255 87 51]
(color/->rgb "rgb(255, 87, 51)") ; CSS RGB string to RGB
;=> [255 87 51]
(color/->hex [255 87 51]) ; RGB to hex
;=> "#ff5733"
(color/->hex "#ff5733") ; hex (passthrough)
;=> "#ff5733"
(color/->hex "rgba(255, 87, 51, 0.8)") ; CSS RGBA string to hex
;=> "#ff5733"
;; Manipulate colors
(color/lighten "#ff5733" 0.2)
;=> "#ffac99"
(color/darken [255 87 51] 0.3)
;=> [153 28 0]
;; Generate palettes
(color/triadic "#ff5733")
;=> ["#33ff58" "#5833ff"]
;; Check accessibility
(color/accessible? "#000000" "#ffffff")
;=> true
(color/contrast-ratio "#ff5733" "#ffffff")
;=> 3.15
;; Generate accessible UI tokens from a brand color
(def theme (color/accessible-theme "#3498db" {:mode :light :level :aa}))
(:primary theme)
;=> "#3498db"
(:on-primary theme)
;=> "#000000"
(color/theme->css-vars theme)
;=> {"--ct-primary" "#3498db", "--ct-on-primary" "#000000", ...}
Use Color records when you want type safety instead of passing raw vectors or
strings through your code:
;; Create Color records from various inputs
(color/color 255 0 0) ; RGB values -> Color record
(color/color "#ff0000") ; Hex string -> Color record
(color/color [255 0 0]) ; RGB vector -> Color record
(color/color [255 0 0 0.8]) ; RGBA vector -> Color record
(color/color "rgb(255, 0, 0)") ; CSS RGB string -> Color record
(color/color "rgba(255, 0, 0, 0.8)") ; CSS RGBA string -> Color record
(color/color (color/color 255 0 0)) ; From existing Color record -> Color record
;; Convert any color type to different formats
(def red-color (color/color 255 0 0))
(color/->hex red-color) ;=> "#ff0000"
(color/->hex "#ff0000") ;=> "#ff0000" ; passthrough
(color/->hex [255 0 0]) ;=> "#ff0000"
(color/->hex "rgb(255, 0, 0)") ;=> "#ff0000"
(color/->rgb red-color) ;=> [255 0 0]
(color/->rgb "#ff0000") ;=> [255 0 0]
(color/->rgb [255 0 0]) ;=> [255 0 0] (passthrough)
(color/->rgb "rgb(255, 0, 0)") ;=> [255 0 0]
(color/->hsl red-color) ;=> [0 100 50]
(color/->hsl "#ff0000") ;=> [0 100 50]
(color/->hsl [255 0 0]) ;=> [0 100 50]
(color/->hsl "rgb(255, 0, 0)") ;=> [0 100 50]
(color/->rgba red-color) ;=> [255 0 0 1.0]
(color/->rgba [255 0 0 128]) ;=> [255 0 0 128] (RGBA passthrough)
(color/->rgba "rgba(255, 0, 0, 0.5)") ;=> [255 0 0 0.5]
;; Color records work with the same functions as strings and vectors
(color/lighten red-color 0.2) ;=> Color record (lighter red)
(color/contrast-ratio red-color (color/color "#ffffff")) ;=> 4.0
;; String representation
(str red-color) ;=> "#ff0000"
(str (color/color 255 0 0 0.5)) ;=> "rgba(255,0,0,0.5)"
The parser accepts common CSS color string formats:
;; CSS RGB strings - various formats supported
(color/->rgb "rgb(255, 87, 51)") ; comma-separated
;=> [255 87 51]
(color/->rgb "rgb(255 87 51)") ; space-separated
;=> [255 87 51]
(color/->rgb "RGB( 255 , 87 , 51 )") ; case-insensitive, extra spaces
;=> [255 87 51]
;; CSS RGBA strings - with alpha channel
(color/->rgba "rgba(255, 87, 51, 0.8)") ; with alpha
;=> [255 87 51 0.8]
(color/->rgba "rgba(255 87 51 0.5)") ; space-separated with alpha
;=> [255 87 51 0.5]
(color/->rgba "RGBA( 255 , 87 , 51 , .7 )") ; decimal alpha, extra spaces
;=> [255 87 51 0.7]
;; CSS strings work with all functions
(color/lighten "rgb(255, 0, 0)" 0.2) ; manipulation
;=> "#ff6666"
(color/accessible? "rgb(255, 255, 255)" "rgb(0, 0, 0)") ; accessibility
;=> true
(color/complementary "rgba(255, 0, 0, 0.5)") ; harmony
;=> "#00ffff"
;; Universal conversion functions accept any supported color input
(color/->rgb "#ff5733") ; hex to RGB
;=> [255 87 51]
(color/->rgb [255 87 51]) ; RGB vector (passthrough)
;=> [255 87 51]
(color/->rgb "rgb(255, 87, 51)") ; CSS RGB string to RGB
;=> [255 87 51]
(color/->rgb (color/color 255 87 51)) ; Color record to RGB
;=> [255 87 51]
(color/->hex [255 87 51]) ; RGB to hex
;=> "#ff5733"
(color/->hex "#ff5733") ; hex (passthrough)
;=> "#ff5733"
(color/->hex "rgb(255, 87, 51)") ; CSS RGB string to hex
;=> "#ff5733"
(color/->hsl [255 87 51]) ; RGB to HSL
;=> [11 100 60]
(color/->hsl "#ff5733") ; hex to HSL
;=> [11 100 60]
(color/->hsl "rgba(255, 87, 51, 0.8)") ; CSS RGBA string to HSL
;=> [11 100 60]
;; Traditional format-specific functions still available
(color/hex->rgb "#ff5733") ; specific hex to RGB
;=> [255 87 51]
(color/rgb->hex [255 87 51]) ; specific RGB to hex
;=> "#ff5733"
;; Normalize short hex codes
(color/normalize-hex "#f53")
;=> "#ff5533"
;; Works with Color records, hex strings, or RGB vectors
(def blue (color/color "#3498db"))
;; Lighten and darken, preserving the input type
(color/lighten blue 0.2) ;=> Color record (lighter blue)
(color/lighten "#3498db" 0.2) ;=> "#8ac4ea"
(color/darken [52 152 219] 0.3) ;=> [18 67 100]
;; Adjust saturation
(color/saturate blue 0.4) ;=> Color record (more saturated)
(color/desaturate "#e74c3c" 0.3) ;=> "#c6675d"
;; Invert colors
(color/invert blue) ;=> Color record (inverted)
(color/invert "#ff0000") ;=> "#00ffff"
;; Complementary colors
(color/complementary "#3498db")
;=> "#db7633"
;; Triadic harmony
(color/triadic "#3498db")
;=> ["#db3398" "#98db33"]
;; Analogous colors
(color/analogous "#3498db" 4 20) ; 4 colors, 20° apart
;=> ["#3360db" "#3e33db" "#7633db" "#ae33db"]
;; Monochromatic palette
(color/monochromatic "#3498db" 5)
;=> ["#003d66" "#066aad" "#1096ef" "#5db4ee" "#a6d4f2"]
;; Generate palettes by strategy
(color/generate-palette :triadic "#e74c3c")
;=> ["#3ce74d" "#4d3ce7"]
(color/generate-palette :split-complementary "#2ecc71" {:angle 25})
;=> ["#8acc2e" "#2ecc56"]
;; Check WCAG compliance
(color/accessible? "#ffffff" "#000000") ; AA standard
;=> true
(color/accessible? "#ffffff" "#777777" :aaa) ; AAA standard
;=> false
(color/accessible? "#ffffff" "#777777" :aa-large) ; AA Large text
;=> true
;; Calculate contrast ratios
(color/contrast-ratio "#ffffff" "#000000")
;=> 21.0
(color/contrast-ratio "#3498db" "#ffffff")
;=> 3.15
;; Find an accessible color by adjusting lightness
(color/find-accessible-color "#3498db" "#e74c3c" :aa)
;=> "#000000" ; Black is the first candidate that meets AA
;; Create WCAG-aware UI tokens from one brand color
(def theme (color/accessible-theme "#3498db" {:mode :light :level :aa}))
(:background theme) ;=> "#f7fbfe"
(:primary theme) ;=> "#3498db"
(:on-primary theme) ;=> "#000000"
(:focus-ring theme) ;=> "#8433db"
;; Use dark mode or stricter contrast
(color/accessible-theme "#ff7a59" {:mode :dark :level :aaa})
;; Export tokens as CSS custom properties
(color/theme->css-vars theme {:prefix "--brand-"})
;=> {"--brand-primary" "#3498db"
; "--brand-on-primary" "#000000"
; "--brand-scale-500" "#3498db"
; ...}
;; Analyze color properties
(color/brightness "#3498db")
;=> 130
(color/dark? "#2c3e50")
;=> true
(color/light? "#ecf0f1")
;=> true
(color/warm? "#e74c3c") ; Red colors are warm
;=> true
(color/cool? "#3498db") ; Blue colors are cool
;=> true
(color/vibrant? "#e74c3c") ; High saturation
;=> true
(color/muted? "#95a5a6") ; Low saturation
;=> true
;; Color distance and similarity
(color/color-distance "#ff0000" "#ff0011")
;=> 17.0
(color/similar? "#ff0000" "#ff0011" 20)
;=> true
;; Convert color names to hex
(color/name->hex "red")
;=> "#ff0000"
(color/name->hex "navy")
;=> "#000080"
;; Find closest color name from any color input type
(color/->name "#ff1100") ; hex string
;=> "red"
(color/->name [255 17 0]) ; RGB vector
;=> "red"
(color/->name (color/color 255 17 0)) ; Color record
;=> "red"
;; Legacy function still available
(color/hex->name "#ff1100")
;=> "red"
;; Mix two colors
(color/mix "#ff0000" "#0000ff") ; 50/50 mix
;=> "#800080"
(color/mix "#ff0000" "#0000ff" 0.25) ; 75% first color, 25% second
;=> "#bf0040"
Key namespaces:
jon.color-tools - Main API with all color manipulation functionsThe tests live in .cljc files and run on both platforms:
# Clojure (JVM)
clojure -T:build test
# ClojureScript (Node.js)
clojure -M:test-cljs
The ClojureScript run needs Node.js on your PATH.
Format all Clojure files using cljfmt:
clojure -Tcljfmt fix
Lint the codebase using clj-kondo:
clojure -Sdeps '{:deps {clj-kondo/clj-kondo {:mvn/version "RELEASE"}}}' -M -m clj-kondo.main --lint src test
# Run CI pipeline and build JAR
clojure -T:build ci
# Install locally
clojure -T:build install
# Deploy to Clojars (requires CLOJARS_USERNAME and CLOJARS_PASSWORD)
clojure -T:build deploy
Open issues, feature requests, or pull requests on GitHub.
These projects influenced the API:
;; Blending modes for design work
(color/blend-multiply "#ff0000" "#0000ff") ; Darkens
;=> "#000000"
(color/blend-screen "#800000" "#000080") ; Lightens
;=> "#800080"
(color/blend-overlay "#ff0000" "#808080") ; Combines multiply/screen
;=> "#ff0000"
;; Tints, shades, and tones for design systems
(color/tint "#3498db" 0.3) ; Mix with white
;=> "#71b7e6"
(color/shade "#3498db" 0.3) ; Mix with black
;=> "#246a99"
(color/tone "#3498db" 0.3) ; Mix with gray
;=> "#4b91c0"
;; Generate series of variations
(color/tints "#e74c3c" 5)
;=> ["#ec7063" "#f1948a" "#f5b7b1" "#fadbd8" "#ffffff"]
(color/shades "#e74c3c" 3)
;=> ["#9a3328" "#4d1914" "#000000"]
;; Alpha blending with source-over compositing
(color/alpha-blend [255 0 0 0.8] [0 0 255 0.6])
;=> [89 0 166 0.92]
(color/with-alpha "#ff0000" 0.5)
;=> "rgba(255,0,0,0.5)"
;; Interpolate between colors
(color/interpolate ["#ff0000" "#0000ff"] 0.5 :rgb)
;=> "#800080" ; Middle point
;; Multi-color interpolation
(color/interpolate ["#ff0000" "#00ff00" "#0000ff"] 0.5 :hsl)
;=> "#00ff00" ; At the green midpoint
;; Generate gradients
(color/gradient ["#ff0000" "#0000ff"] 5 :rgb)
;=> ["#ff0000" "#bf0040" "#800080" "#4000bf" "#0000ff"]
;; Use HSL for more natural color transitions
(color/gradient ["#ff0000" "#ffff00"] 3 :hsl)
;=> ["#ff0000" "#ff8000" "#ffff00"]
;; Use perceptual distance instead of RGB distance
(color/delta-e "#ff0000" "#fe0000")
;=> 0.37 ; Under 1.0: imperceptible
(color/delta-e "#ff0000" "#0000ff")
;=> 176.3 ; Large difference
;; Check perceptual similarity
(color/perceptually-similar? "#ff0000" "#fe0101")
;=> true ; Within JND threshold
(color/perceptually-similar? "#ff0000" "#0000ff")
;=> false
;; Convert to LAB color space
(color/rgb->lab [255 0 0])
;=> [53.23 80.11 67.2]
Use rgb->oklab with sRGB channels from 0 to 255. You get [l a b]
coordinates with lightness on a 0 to 1 scale. Use ->rgb first for hex
strings, CSS strings, or Color records. The conversion preserves full
precision; use round-coords to format coordinates for display.
(-> "#336699"
color/->rgb
color/rgb->oklab
(color/round-coords {:decimal-places 4}))
;=> [0.4993 -0.033 -0.093]
;; Convert back to sRGB or hex
(color/oklab->hex [0.4993 -0.033 -0.093])
;=> "#336699"
(color/in-gamut? [0.9 0.3 0.3])
;=> false
;; OKLCh: lightness, chroma, hue in degrees
(color/round-coords (color/rgb->oklch [255 0 0]) {:decimal-places 4})
;=> [0.628 0.2577 29.2339]
(color/oklch->hex [0.628 0.2577 29.23])
;=> "#ff0000"
oklab->rgb returns unrounded, unclamped channels; oklab->hex clamps colors
outside sRGB. See the API reference
for axis definitions, gamut handling, and input requirements.
;; Convert temperature to color (1000K - 40000K)
(color/kelvin->rgb 1800) ; Candlelight
;=> [255 126 0]
(color/kelvin->hex 3000) ; Warm white
;=> "#ffb16e"
(color/kelvin->rgb 6500) ; Daylight
;=> [255 254 250]
(color/kelvin->color 10000) ; Cool/blue
;=> Color record
;; Approximate temperature from color
(color/rgb->kelvin (color/kelvin->rgb 1800))
;=> 1805 ; Close to the original 1800K
Color Spaces:
Thanks to the Clojure community for the tooling and libraries behind this project.
See CHANGELOG.md for version history and changes.
Copyright © 2025 Jon
Distributed under the Eclipse Public License version 1.0.
The Eclipse Public License is available at http://www.eclipse.org/legal/epl-v10.html
The repository runs GitHub Actions checks on every branch. Deployments to Clojars are triggered in two ways:
Deployments run from the master branch after the Checks workflow completes
and the commit message contains [deploy].
Example:
git commit -m "Release version 1.1.0 [deploy]"
git push origin master
To deploy from GitHub, open the "Deploy to Clojars" workflow and click "Run workflow".
Requirements:
CLOJARS_USERNAME and CLOJARS_PASSWORD are set in the repository secrets[deploy] in your commit message when pushing to masterCan 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 |