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

20 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 — 340 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

340 test cases derived from tailwind-merge.

LibraryTailwindConformance
winnowv4.x340/340
tailwind-merge-cljv3.4218/340

Performance

Apple M4, just bench:

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

Coverage

666 patterns. Tailwind 4.x. See supported-classes.org.

Automated upstream detection runs daily via GitHub Actions to detect new utilities added to Tailwind CSS.

Platforms

PlatformStatus
Clojure (JVM)
ClojureScript
Babashka

Development

just               # Full test suite (required before commits)
just bench         # Run benchmarks
just docs          # Regenerate supported-classes.org
just upstream-check  # Check for upstream Tailwind changes
just upstream-update # Update baseline from Tailwind CSS

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.