https://github.com/jcf/winnow/actions/workflows/ci.yml/badge.svg https://img.shields.io/clojars/v/dev.jcf/winnow.svg
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.org
AGPL-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.