Bridge shadcn/ui React components to ClojureScript with compile-time component resolution.
shadcn-cljs is a build-time code generator that reads your components.json and tsconfig.json, scans your compiled shadcn component files, and produces a .cljc namespace with:
- A compile-time macro (
$sui) for keyword→component resolution - Runtime resolution for dynamic component lookup
- Auto-discovered hooks and
cnutility
Works with UIx and Helix. Only runs on the JVM during shadow-cljs compilation.
Assumes a working shadcn/ui + TypeScript compilation setup. See Prerequisites if you're starting from scratch.
In your deps.edn (:dev alias):
io.github.doomsun-dev/shadcn-cljs {:git/url "https://github.com/doomsun-dev/shadcn-cljs.git"
:sha "..."}In your shadow-cljs.edn:
:build-hooks [(shadcn-cljs.hook/shadow-hook
{:output-ns "lib.shadcn-ui"
:output-dir "src/cljs"})]That's it. All paths are inferred from components.json and tsconfig.json.
(ns my-app.ui
(:require [lib.shadcn-ui :refer [$sui cn]]))
;; Compile-time component resolution
($sui :button {:variant "outline"} "Click me")
($sui :card-header {} "Title")
;; Dynamic resolution
($sui some-variable {:class (cn "p-4" "bg-muted")})| Key | Required | Default | Description |
|---|---|---|---|
:output-ns |
No | "shadcn-cljs.ui" |
Namespace of generated file |
:output-dir |
No | "src/main" |
Directory to write the generated .cljc file (must be on classpath) |
:wrapper |
No | :uix |
React wrapper library (:uix or :helix) |
:macro-name |
No | "$sui" |
Name of the generated macro |
:components-json |
No | "components.json" |
Path to shadcn config file |
:tsconfig |
No | "tsconfig.json" |
Path to TypeScript config |
All other paths (components dir, import prefix, utils, hooks) are derived from components.json + tsconfig.json.
Hooks are handled automatically:
- Standalone hooks (in the hooks directory) are auto-discovered by reading JS exports
- Known component hooks (like
useSidebar) are built into the library
For libraries beyond UIx/Helix:
{:wrapper :custom
:wrapper-config {:dollar-sym 'my.lib/$
:require '[my.lib :refer [$] :rename {$ my-$}]
:display-name "MyLib"}}Separate hook for icon libraries like lucide-react:
:build-hooks
[(shadcn-cljs.hook/shadow-hook
{:output-ns "lib.shadcn-ui"
:output-dir "src/cljs"})
(shadcn-cljs.icons.hook/shadow-hook
{:output-ns "lib.lucide-react"
:output-dir "src/cljs"
:npm-package "lucide-react"})]Usage:
(ns my-app.ui
(:require [lib.lucide-react :refer [$lucide]]))
($lucide :panel-left {:class "size-4"})
($lucide :bell)shadcn-cljs reads your existing components.json and tsconfig.json to find everything it needs. You need a TypeScript compilation pipeline that gets shadcn components from source to JS output before the hook runs.
your-project/
├── components.json # shadcn CLI config (shadcn-cljs reads this)
├── tsconfig.json # TypeScript config (shadcn-cljs reads this)
├── src/main/js/shadcn/ # TypeScript source (shadcn CLI writes here)
│ ├── components/ui/
│ ├── hooks/
│ └── lib/utils.ts
├── src/main/gen/shadcn/ # Compiled JS output (tsc writes here)
│ ├── components/ui/ # shadcn-cljs scans these
│ ├── hooks/
│ └── lib/utils.js
└── src/main/cljs/ # Generated .cljc files land here
Configure tsconfig.json so TypeScript compiles shadcn source into a gen/ output directory:
{
"compilerOptions": {
"target": "ES2017",
"outDir": "./src/main/gen",
"rootDir": "./src/main/js",
"module": "commonjs",
"moduleResolution": "node",
"jsx": "react-jsx",
"baseUrl": "./",
"paths": {
"@/shadcn/*": ["./src/main/js/shadcn/*"],
"@/*": ["./src/main/js/*"]
}
},
"include": ["src/main/js/**/*.tsx", "src/main/js/**/*.ts"],
"exclude": ["node_modules", "src/main/gen"]
}Run tsc --build && tsc-alias before starting shadow-cljs.
Tell shadow-cljs where to find your compiled JS:
;; shadow-cljs.edn
:js-options {:js-package-dirs ["node_modules" "src/main/gen"]}npx shadcn add button card dialogAfter adding components, recompile TypeScript and restart shadow-cljs. The build hook detects new .js files and regenerates automatically.
The generated macros need a :lint-as entry in your .clj-kondo/config.edn. Map to your wrapper's $ macro:
UIx:
{:lint-as {lib.shadcn-ui/$sui uix.core/$
lib.lucide-react/$lucide uix.core/$}}Helix:
{:lint-as {lib.shadcn-ui/$sui helix.core/$
lib.lucide-react/$lucide helix.core/$}}Adjust the namespace if you customized :output-ns or :macro-name.
- Config: Reads
components.json+tsconfig.jsonto derive all paths - Discovery: Scans the components and hooks directories for
.jsfiles - Generation: Produces a
.cljcfile with CLJ macros + CLJS runtime code - Staleness: Only regenerates when the output file is missing or older than the components directory
- Macro:
$suiresolves component keywords to JS module property access at compile time - Runtime:
resolve-componenthandles dynamic keyword lookup for cases where the tag isn't a literal
MIT