What Is ClojureScript?
ClojureScript is a compiler that translates Clojure's syntax and core semantics into JavaScript, so the same immutable persistent data structures, functional idioms, and macro system that Clojure offers on the JVM also run in browsers and on Node.js. It shares the vast majority of clojure.core with Clojure, so most everyday code — sequence functions, destructuring, protocols — works identically on both platforms.
Cricket analogy: ClojureScript is like a star Test batsman adapting their exact same technique to succeed in T20 cricket — same grip, same core principles, translated for a different pitch and format rather than the original arena.
Setting Up and Compiling with shadow-cljs
shadow-cljs is the most widely used modern build tool for ClojureScript, configured via shadow-cljs.edn. The :builds map names one or more build targets, such as an :app browser build or a :test Node build, and running the dev server connects a REPL directly to the running browser tab, so evaluating a form updates the live page and hot-reloads changed code without a full page refresh.
Cricket analogy: A shadow-cljs.edn's :builds config is like a tour manager's master schedule listing each format's separate training plan, with the connected REPL acting like a live coach who can tweak a batsman's technique mid-net-session without stopping the whole session.
;; shadow-cljs.edn
{:source-paths ["src" "test"]
:dependencies [[reagent "1.2.0"]]
:builds
{:app {:target :browser
:output-dir "public/js"
:modules {:main {:init-fn myapp.core/init!}}}
:test {:target :node-test
:output-to "out/test.js"}}}
;; src/myapp/core.cljs
(ns myapp.core
(:require [clojure.string :as str]))
(defn ^:export init! []
(js/console.log "app starting")
(let [el (.getElementById js/document "app")
greeting (str/upper-case "hello from clojurescript")]
(set! (.-textContent el) greeting)))
;; JS <-> Clojure data conversion at a boundary
(defn send-payload! [clj-map]
(js/fetch "/api/save"
#js {:method "POST"
:body (js/JSON.stringify (clj->js clj-map))}))
JavaScript Interop in ClojureScript
ClojureScript reuses the same dot-notation convention as Clojure/Java interop: js/ prefixes access to any global JavaScript object such as js/window or js/document, (.-propName obj) reads a JS property, and (.method obj args) calls a JS method. For data crossing the boundary, #js {...} converts a Clojure literal into a plain JS object at compile time, while clj->js and js->clj convert whole nested structures at runtime.
Cricket analogy: Reaching into js/window.location is like an overseas commentator citing a specific foreign broadcaster's live feed by its full network name to grab a stat, and reading an innings property off a scoreboard object reads a number directly rather than computing it.
js/ is the prefix for reaching any global JavaScript object (js/window, js/document, js/Math). Use (.-propName obj) for property access and (.method obj args) for method calls — the same dot syntax as Clojure/Java interop. #js {...} converts a Clojure map literal to a plain JS object at compile time; for converting whole nested structures at runtime, use clj->js and js->clj (with :keywordize-keys true to get keyword keys back).
Differences from Clojure and Common Gotchas
A few platform differences matter in day-to-day ClojureScript development. JavaScript's single-threaded event loop means there are no real OS threads, so concurrency-flavored code relies on core.async's go blocks and channels instead. ClojureScript numbers compile to JavaScript's native doubles, so Clojure's arbitrary-precision integers and exact ratios don't exist — a real concern for money math. Reader conditionals, #?(:clj expr-a :cljs expr-b), let a .cljc file share the bulk of its logic across both platforms while branching only where they genuinely differ.
Cricket analogy: ClojureScript's JS numbers are all doubles with no arbitrary-precision integers or ratios, similar to how a scoreboard operator rounds a very long partnership tally to fit the display; reader conditionals are like a coach's playbook with a Test-match page and a separate T20 page for the same underlying strategy.
ClojureScript numbers compile to JavaScript's native double-precision float — there's no arbitrary-precision integer or exact ratio type like Clojure has on the JVM. Money and other precision-sensitive calculations need explicit handling (e.g. integer cents) rather than relying on ClojureScript numeric literals to stay exact.
- ClojureScript compiles Clojure syntax and semantics to JavaScript, running in browsers, Node.js, and other JS runtimes.
- shadow-cljs (or the older Figwheel) is the standard tool for compiling ClojureScript, connecting a REPL to a running browser, and hot-reloading code.
- js/ prefixes access to any global JavaScript object; (.-prop obj) reads a JS property; (.method obj args) calls a JS method — the same dot syntax used for JVM interop.
- #js {...} converts a Clojure map/vector literal to a plain JS object/array at compile time; clj->js and js->clj convert whole nested structures at runtime.
- ClojureScript runs on JavaScript's single-threaded event loop; core.async's go blocks provide cooperative, channel-based concurrency instead of real OS threads.
- ClojureScript numbers are JS doubles — there's no arbitrary-precision integer or exact ratio type, which matters for precision-sensitive math.
- Reader conditionals, #?(:clj expr-a :cljs expr-b), let a single .cljc file share logic between Clojure and ClojureScript, branching per platform where needed.
Practice what you learned
1. What does ClojureScript compile to?
2. How do you access a global JavaScript object like the browser window in ClojureScript?
3. What is the purpose of clj->js?
4. Why does ClojureScript use core.async's go blocks instead of real threads for concurrency?
5. What is a reader conditional like #?(:clj (foo) :cljs (bar)) used for?
Was this page helpful?
You May Also Like
Clojure and Java Interop
Understand how Clojure calls Java classes and methods directly via dot notation, implements interfaces with proxy/reify/gen-class, and handles exceptions and type hints for performance.
Namespaces and require
Learn how Clojure organizes code into namespaces and how ns, require, import, and refer control how symbols are loaded and referenced across files.
Leiningen and deps.edn
Compare Clojure's two main build tools — Leiningen's project.clj and the official tools.deps CLI's deps.edn — and learn how each manages dependencies, profiles, and aliases.
Related Reading
Related Study Notes in Programming
Browse all study notesApache Spark Study Notes
Programming · 30 topics
ProgrammingApache Flink Study Notes
Programming · 30 topics
ProgrammingHadoop Study Notes
Programming · 30 topics
ProgrammingSnowflake Study Notes
Programming · 30 topics
ProgrammingApache Airflow Study Notes
Programming · 30 topics
Programmingdbt (Data Build Tool) Study Notes
Programming · 30 topics