Skip to content
/ winnow Public

Tailwind CSS class merging for Clojure.

License

Notifications You must be signed in to change notification settings

jcf/winnow

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

13 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Winnow

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.

What It Does

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"

Principles

  • Predictable — Deterministic output. Same input always produces same result.
  • Strict — Requires explicit configuration. Won’t guess that bg-brand is 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.

Installation

dev.jcf/winnow {:git/url "https://github.com/jcf/winnow"
                :git/sha "LATEST"}

API

(require '[winnow.api :as winnow])

resolve

(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.

normalize

(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"

make-resolver

;; 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)

Conformance

257 test cases derived from tailwind-merge.

LibraryTailwindConformance
winnowv4.1257/257
tailwind-merge-cljv3.4218/257

Performance

Apple M4, just bench:

ScenarioClassesTime
Small2945 ns
Medium105.49 µs
Large2512.5 µs

Coverage

445 patterns. Tailwind 4.1. See supported-classes.org.

Platforms

PlatformStatus
Clojure (JVM)
ClojureScript
Babashka

Development

just          # Full test suite (required before commits)
just bench    # Run benchmarks
just docs     # Regenerate supported-classes.org

License

AGPL-3.0. See LICENSE.

Support

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.