Skip to content
/ bud Public

A minimalist ClojureScript DOM library with precise, signal-driven reactivity for single-page applications

Notifications You must be signed in to change notification settings

mtmr0x/bud

Repository files navigation

Bud

Bud is a minimalist DOM library for ClojureScript. It lets you build reactive single-page applications using real DOM elements. No Virtual DOM, no Shadow DOM, no magic compiler.


⚡ Why Bud?

  • Built directly on top of vanilla JS — no abstraction layers.
  • Uses native DOM APIs for actual elements, not proxies or clones.
  • Embraces signals for composable, explicit reactivity.
  • No Shadow DOM. No Virtual DOM. Just DOM.

Bud is to reactivity what Clojure is to state: explicit, simple, and powerful (although still working on this last one 😅).


Table of Contents

Project status

🧷 The API is stable and used in production, though not battle-tested yet. Bug fixes, performance tweaks, and features will evolve with real-world use and feedback.

Try it, break it, file issues, and send feedback.

Installation

Clojars Project

Features and usage

The core of Bud consists in just 3 functions, create-signal, reactive-fragment, and dom-render. The rest of the problems to build reactive UIs is built-in for great ergonomics.

  • create-signal creates a signal, which is a reactive value that can be used in the DOM.
  • reactive-fragment is a function that takes a function and returns a reactive fragment, which is a piece of DOM that will be updated when the signals it uses change.
  • dom-render is a function that takes a DOM element and a component function, and renders the component into the DOM element.

Creating signals (reactive text nodes)

(ns bud.example
  (:require
    [bud.core :as bud]))

(defn app []
  (let [[get! set!] (bud/create-signal "world")]
    [:h1 "Hello, " get! "!"]))

Text nodes update automatically when signals change.

Reactive fragments

(defn app []
  (let [[get! _] (bud/create-signal {:value 42})]
    [:div 
     [:h1 "Hello, world!"]
     (bud/reactive-fragment #(vector :h2 (str "Value: " (:value (get!)))))]))

Wrap dynamic expressions in reactive-fragment when they go beyond simple strings/numbers.

Dynamic collections

(defn list-component []
  (map #(into [] [:div %]) ["a" "b" "c"]))

You can use map, loops, conditionals — it's just Clojure data.

Event handling

(defn app []
  (let [[getter! setter!] (bud/create-signal "world")]
    [:div
     [:input {:type "text"
              :value getter!
              :on-input #(setter! (.. % -target -value))}]
     [:p "Current value: " getter!]]))

Event listeners are auto-wired via :on-*.

Reactive conditional rendering

(defn app []
  (let [[get-value! set-value!] (bud/create-signal "world")]
    [:div
     (bud/reactive-fragment
       #(when (= (get-value!) "world")
          [:p "This only shows if the value is 'world'"]))]))

Only renders when the condition is true — rerenders on signal change.

Composing components

(defn footer-component [value]
  [:footer
   [:p "This is a footer component. " value]])

(defn app []
    (let [[get-value! set-value!] (bud/create-signal "")]
        [:div
         [:h1 "Hello, " get-value! "!"]
         [:input {:type "text"
                :value get-value!
                :on-input #(set-value! (.. % -target -value))}]
         ;; footer component
         [footer-component get-value!]]))

Ref attribute

(defn editor-js []
  (let [editor-instance (atom nil)]
    [:div
     [:h2 "EditorJS Example"]
     [:div {:class "editor-js"
            :id "editorjs"
            :ref (fn [el] ;; ⬅️  ref attribute must a function and 
                   ;;;;;;;;; it will be called passing the DOM element to it
                   (when el
                     (let [e (js/EditorJS. #js {:autofocus true})]
                       (reset! editor-instance e))))}]]))

You can use the ref attribute to get a reference to a DOM element when the component is rendered. This is useful for direct DOM manipulations or integrations with other libraries.

Style attribute

(defn styled-component []
  [:div {:style {:color "blue"
                 :font-size "20px"}}
   "This is a styled component."
   [:span {:style "font-weight: bold;"} " Bold text inside."]])

You can use the style attribute to apply styles to elements, both string and map are accepted values.

Rendering your app

(ns bud.example 
    (:require
        [bud.core :as bud]))

;; { ... your app code ... }

(defn ^:dev/after-load start []
  (let [el (js/document.getElementById "app")]
    (bud/dom-render el app)))

Example app

(ns bud.example
  (:require
    [bud.core :as bud]))

(defn footer-component [value]
  ;; the value in the attr-test will not be reactive
  ;; unless you use it in side a reactive-fragment
  [:footer {:attr-test value} ;; <- if it's a signal, it will be reactive
   [:p "This is a footer component. " value]])

(defn app []
  (let [[get-value! set-value!] (bud/create-signal {:value "world"})
        [get-string-value! set-string-value!] (bud/create-signal "world")]

    [:div
     ;; string or number values will be rendered as
     ;; reactive text nodes, so you can use them directly
     [:h1 "Hello, " get-string-value! "!"]

     ;; you can do maps!
     (map #(into [] [:div %]) '("a" "b" "c" "d" "e"))

     ;; when the value of the signal is not a string or a number,
     ;; it will not be rendered as a reactive text node, so you
     ;; need to enclose it in a reactive-fragment
     (bud/reactive-fragment
       #(do [:h2 (str "This is a reactive app. Current value: " (:value (get-value!)))]) )

     [:p "Type something below:"]
     [:input {:type "text"
              ;; works because get-value has a default value.
              ;; if this value needs to be reactive, use 
              ;; reactive-fragment to wrap the input
              :value (:value (get-value!))
              ;; on- attributes will create event listeners automatically
              :on-input #(do
                            (set-string-value! (.. % -target -value))
                            (set-value! {:value (.. % -target -value)}))}]

     (bud/reactive-fragment
       #(when (= (get-string-value!) "world")
          [:div {:attr-test (get-string-value!)}
           [:p "this only shows if the word in the input is \"world\""]
           [:p "input value: " get-string-value!]]))

     ;; get-value! is a signal, so it will be reactive
     ;; in any inner scope since kept as a signal and
     ;; used directly in the DOM as text node. If you
     ;; want to use it as a reactive html attribute,
     ;; follow the example after this one.
     [footer-component get-value!]]))

(defn ^:dev/after-load start []
  (let [el (js/document.getElementById "app")]
    (bud/dom-render el app)))

Roadmap

Roadmap to 0.2.0 (which will be a beta release)

  • Find a good way to manage the ref problem for rendering libs in the DOM
  • Parse the style attribute to accept both string and maps

Roadmap to 0.3.0

  • Make reactive-fragment doesn't return a container div and improve its ergonomics
  • Evaluate when attribute is a signal and decide if it should be reactive or throw an error

Roadmap to 0.4.0

  • Wildly test it and fix any bugs

About

A minimalist ClojureScript DOM library with precise, signal-driven reactivity for single-page applications

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published