Components are the building blocks of su applications. Each component becomes a native Custom Element with Shadow DOM isolation.
Use defc to define a component:
(ns my.app
(:require [su.core :refer [defc]]))
(defc my-greeting []
[:p "Hello, world!"])This registers a Custom Element <my-greeting> that renders a paragraph.
Component names must contain a hyphen (Web Component requirement):
;; Good
(defc my-button [] ...)
(defc task-item [] ...)
;; Bad — will throw an error
(defc button [] ...)Components receive props as HTML attributes. Declare them in the options map:
(defc greeting-card
{:props {:name "string" :age "number" :active "boolean"}}
[{:keys [name age active]}]
[:div
[:h2 (str "Hello, " name)]
[:p (str "Age: " age)]
(when active [:span "Active"])])| Type | HTML Attribute | ClojureScript Value |
|---|---|---|
"string" |
"Alice" |
"Alice" |
"number" |
"42" |
42 |
"boolean" |
"true", "" |
true |
HTML attributes are always strings. su deserializes them according to the declared type.
The props argument is a Kiso HashMap with keyword keys. Use :keys
destructuring:
(defc user-card
{:props {:name "string" :role "string"}}
[{:keys [name role]}]
[:div
[:strong name]
[:span (str " (" role ")")]])Unlike React, the component function in su runs once during initialization. This is the Solid.js model:
(defc my-timer []
;; This code runs ONCE when the component mounts
(let [seconds (atom 0)]
(js/console.log "Component initialized!")
;; The hiccup is automatically reactive — @seconds triggers re-renders
[:div (str "Seconds: " @seconds)]))defc auto-wraps the final hiccup expression in a reactive render function.
Dereferencing atoms (@seconds) inside the hiccup body automatically subscribes
to changes — no explicit fn return needed.
For setup code that must run once (timers, event listeners, etc.), explicitly
return a fn:
(defc my-timer []
(let [seconds (atom 0)
id (js/setInterval #(swap! seconds inc) 1000)]
(on-unmount #(js/clearInterval id))
;; Explicit fn: setup code above runs once, render fn re-runs on changes
(fn []
[:div (str "Seconds: " @seconds)])))When defc detects an explicit fn return, it skips auto-wrap.
Regular props are serialized as HTML attributes (strings only). For passing rich values like atoms, objects, or collections, use Props Channeling:
(defc task-list
{:props {:tasks :atom}}
[{:keys [tasks]}]
[:ul
(map (fn [t] [:li (:text t)])
@tasks)])The :atom prop type tells su to pass the value as a JavaScript property
instead of an HTML attribute. This preserves the original value without
serialization.
Pass the atom in hiccup:
(let [tasks (atom [{:text "Buy milk"}])]
[::task-list {:tasks tasks}])Use :: (namespace-qualified keyword) to reference a component defined in
the current namespace:
(defc child-comp [] [:p "I am a child"])
(defc parent-comp []
[:div
[::child-comp]])Use a fully qualified keyword:
(ns my.page
(:require [my.widgets :as w]))
(defc my-page []
[:div
[:my.widgets/fancy-button {:label "Click me"}]])Components nest naturally via hiccup:
(defc page-header []
[:header [:h1 "My App"]])
(defc page-footer []
[:footer [:p "2024 My App"]])
(defc app-shell []
[:div
[::page-header]
[:main [:p "Content goes here"]]
[::page-footer]])Use mount to render a component tree into a DOM container:
(ns my.app
(:require [su.core :as su]))
(su/mount (js/document.getElementById "app")
[::my-app])mount takes two arguments:
- A DOM element (the container)
- A hiccup vector referencing the root component
When a component has no props, the empty vector [] is still required:
(defc simple-widget []
[:div "No props needed"])| Concept | Description |
|---|---|
defc |
Define a Custom Element with Shadow DOM |
| Props | Declared in {:props {...}}, destructured |
| Prop types | "string", "number", "boolean", :atom |
| Component function | Runs once (setup phase) |
| Auto-wrap | Final hiccup is auto-wrapped for reactive updates |
| Form-2 | Explicit fn return for advanced setup patterns |
::name |
Reference component in same namespace |
mount |
Render component tree into a DOM container |