Tailwind CSS class merging for Clojure, ClojureScript, and Babashka.
Winnow resolves conflicting Tailwind classes by keeping the last value for each utility group. We process classes left to right; later classes override earlier ones in the same group.
(winnow/resolve ["px-2 py-4" "px-6"])
;; => "py-4 px-6"
;; ↑ ↑
;; │ └─ px-6 overrides px-2 (same group: padding-x)
;; └─────── py-4 preserved (different group: padding-y)Modifiers create separate groups. hover:p-4 and p-4 don’t conflict:
(winnow/resolve ["p-2 hover:p-2" "p-4"])
;; => "hover:p-2 p-4"Unknown classes pass through unchanged:
(winnow/resolve ["my-thing" "block" "hidden"])
;; => "my-thing hidden"- Predictable — Deterministic output. Same input always produces same result.
- Strict — Requires explicit configuration. Won’t guess that
bg-brandis a color unless you tell it. - Stable — API designed for production. Breaking changes are versioned.
- Tested — 257 conformance tests, generative property tests, clojure.spec validation.
- Fast — Sub-microsecond for typical inputs. Benchmarked with Criterium.
- Pure Clojure — No JavaScript runtime. No external dependencies.
dev.jcf/winnow {:git/url "https://github.com/jcf/winnow"
:git/sha "LATEST"}(require '[winnow.api :as winnow])(winnow/resolve ["p-4" "p-[10px]"]) ;; => "p-[10px]"
(winnow/resolve ["bg-red-500" "bg-(--x)"]) ;; => "bg-(--x)"
(winnow/resolve ["pt-2 pr-2 pb-2 pl-2" "p-4"]) ;; => "p-4"Requires a vector. Use normalize for flexible input.
(def tw (comp winnow/resolve winnow/normalize))
(tw nil) ;; => ""
(tw "p-4 m-2") ;; => "p-4 m-2"
(tw ["base" nil "override"]) ;; => "base override"
(tw [["a"] ["b" "c"]]) ;; => "a b c";; Custom colors
(def resolve (winnow/make-resolver {:colors #{"primary" "surface"}}))
(resolve ["bg-red-500" "bg-primary"]) ;; => "bg-primary"
;; Class prefix
(def resolve (winnow/make-resolver {:prefix "tw-"}))
(resolve ["tw-px-2 tw-px-4"]) ;; => "tw-px-4"
(resolve ["px-2 px-4"]) ;; => "px-2 px-4" (no prefix, passes through)257 test cases derived from tailwind-merge.
| Library | Tailwind | Conformance |
|---|---|---|
| winnow | v4.1 | 257/257 |
| tailwind-merge-clj | v3.4 | 218/257 |
Apple M4, just bench:
| Scenario | Classes | Time |
|---|---|---|
| Small | 2 | 945 ns |
| Medium | 10 | 5.49 µs |
| Large | 25 | 12.5 µs |
445 patterns. Tailwind 4.1. See supported-classes.org.
| Platform | Status |
|---|---|
| Clojure (JVM) | ✓ |
| ClojureScript | ✓ |
| Babashka | ✓ |
just # Full test suite (required before commits)
just bench # Run benchmarks
just docs # Regenerate supported-classes.orgAGPL-3.0. See LICENSE.
Winnow is maintained by James Conroy-Finn. If you find it useful, consider sponsoring my work on GitHub.
For commercial licensing or consulting inquiries, email james@invetica.co.uk.