Skip to content

Clojure Guide — Hiccup: HTML Templating in Clojure

DodaTech Updated 2026-06-28 5 min read

In this tutorial, you will learn about Clojure Guide. We cover key concepts, practical examples, and best practices to help you master this topic.

Hiccup is a library for representing HTML in Clojure using vectors for elements, maps for attributes, and functions for reusable components, eliminating the need for a separate templating language.

What You'll Learn

  • Hiccup syntax: vectors, keywords, and maps
  • Dynamic content generation
  • Reusable component functions
  • HTML attributes and CSS classes
  • Integration with Ring

Why It Matters

Hiccup keeps your view logic in Clojure. No separate template language, no context switching. Components are just functions. Doda Browser uses Hiccup for its internal dashboard rendering.

Real-World Use

Luminus web framework uses Hiccup by default. Many Clojure web apps choose Hiccup for its simplicity and the full power of Clojure for generating dynamic HTML.

flowchart LR
    A["Hiccup"] --> B["Vectors as Elements"]
    B --> C["Attributes"]
    C --> D["Components"]
    D --> E["Integration"]
    A:::current --> B
    style A fill:#2563eb,stroke:#2563eb,color:#fff
    style B fill:#dbeafe,stroke:#2563eb,color:#1e40af
    style C fill:#dbeafe,stroke:#2563eb,color:#1e40af
    style D fill:#dbeafe,stroke:#2563eb,color:#1e40af
    style E fill:#f1f5f9,stroke:#94a3b8,color:#64748b

Basic Hiccup Syntax

(require '[hiccup.core :as h])

;; Simple element
(h/html [:h1 "Hello World"])
;; => "<h1>Hello World</h1>"

;; Element with attributes
(h/html [:div {:class "container" :id "main"}
          [:p "Content"]])
;; => "<div class=\"container\" id=\"main\"><p>Content</p></div>"

HTML Elements

;; Standard elements
[:h1 "Title"]
[:p "Paragraph"]
[:a {:href "/link"} "Link"]
[:img {:src "image.jpg" :alt "Image"}]

;; Self-closing tags
[:br]
[:hr]
[:input {:type "text" :name "email"}]

;; Nested elements
[:div
  [:h2 "Section"]
  [:p "Paragraph one"]
  [:p "Paragraph two"]]

Dynamic Content

;; Using Clojure functions for dynamic data
(defn user-card [user]
  [:div.user-card
   [:h3 (:name user)]
   [:p (:email user)]
   [:span.badge (if (:active user) "Active" "Inactive")]])

;; Render with data
(h/html (user-card {:name "Alice" :email "alice@example.com" :active true}))

Component Functions

;; Reusable layout
(defn layout [title & body]
  (h/html
   [:html
    [:head [:title title]]
    [:body
     [:nav {:class "navbar"}]
     [:main.container body]
     [:footer "DodaTech"]]]))

;; Page component
(defn home-page []
  (layout "Home"
    [:h1 "Welcome"]
    [:p "This is the home page."]))

;; With data
(defn user-list [users]
  (layout "Users"
    [:h1 "User Directory"]
    [:table
     (for [user users]
       [:tr
        [:td (:name user)]
        [:td (:email user)]])]))

Forms

(defn login-form []
  [:form {:action "/login" :method "POST"}
   [:div
    [:label {:for "email"} "Email"]
    [:input {:type "email" :id "email" :name "email"}]]
   [:div
    [:label {:for "password"} "Password"]
    [:input {:type "password" :id "password" :name "password"}]]
   [:button {:type "submit"} "Login"]])

CSS and Classes

;; CSS class shorthand
[:div.my-class "Content"]    ; Same as [:div {:class "my-class"} "Content"]

;; Multiple classes
[:div.class1.class2 "Content"]

;; Inline styles
[:div {:style {:color "red" :font-size "16px"}} "Styled"]

;; CSS id shorthand
[:div#my-id "Content"]

HTML Escaping

;; Safe by default - Hiccup escapes content
(h/html [:p "<script>alert('xss')</script>"])
;; => "<p>&lt;script&gt;alert('xss')&lt;/script&gt;</p>"

;; Raw HTML (unescaped)
(h/html [:p (h/raw-string "<strong>Bold</strong>")])

Hiccup with Ring

(require '[hiccup.core :as h])
(require '[ring.adapter.jetty :as jetty])

(defn handler [request]
  {:status 200
   :headers {"Content-Type" "text/html"}
   :body (h/html
          [:html
           [:body
            [:h1 "Ring + Hiccup"]
            [:p "Server time: " (java.util.Date.)]]])})

(jetty/run-jetty handler {:port 8080 :join? false})

Common Mistakes

1. Forgetting h/html wrapper

Hiccup vectors are data. You must pass them through h/html to generate the actual HTML string.

2. Not escaping user input

Hiccup escapes by default, but h/raw-string disables escaping. Use it only with trusted content.

3. Using mutable state in components

Components are pure functions. Don't use atoms or refs inside render functions. Pass all data as parameters.

4. Deeply nested components

Break large components into smaller functions. Each function should render one logical section.

5. Mixing Hiccup and string concatenation

Build HTML entirely with Hiccup vectors. String concatenation bypasses escaping and creates maintenance issues.

Practice Questions

1. How does Hiccup represent HTML? HTML elements are vectors, where the first element is the tag name (keyword), the optional second is an attribute map, and remaining elements are children.

2. Is Hiccup safe against XSS? Yes, Hiccup escapes all string content by default. Only use raw-string when you explicitly need unescaped output.

3. How do you create reusable components? Components are just functions that return Hiccup vectors. Pass data as parameters and compose them like any Clojure function.

Challenge: Build a reusable table component that accepts column definitions and row data.

FAQ

{{< faq question="Is Hiccup faster than JSP or Thymeleaf?" >}} Hiccup is comparable to other templating engines. Since it's pure Clojure, the JIT compiles hot paths efficiently. For high throughput, consider Caching rendered pages. {{< /faq >}}

{{< faq question="Can I use Hiccup with JavaScript frameworks?" >}} Hiccup generates server-side HTML. For client-side interactivity, pair it with HTMX, Alpine.js, or Reagent for full ClojureScript. {{< /faq >}}

{{< faq question="Does Hiccup support layouts?" >}} No built-in layout system. Use function composition: a layout function wraps page content, which wraps sections. {{< /faq >}}

{{< faq question="How do I include JavaScript and CSS in Hiccup?" >}} Use standard script and link elements: [:script {:src "app.js"}] and [:link {:rel "stylesheet" :href "style.css"}]. {{< /faq >}}

{{< faq question="Can Hiccup generate XML?" >}} Base Hiccup generates HTML5. For general XML, use hiccup.core/hiccup with :mode or use a dedicated XML library. {{< /faq >}}

Mini Project

Build a blog page component system with Hiccup:

(defn blog-post [post]
  [:article.post
   [:h2 (:title post)]
   [:p.meta (str "By " (:author post) " on " (:date post))]
   [:div.content (:body post)]])

(defn blog-layout [title posts]
  (h/html
   [:html
    [:head [:title title]]
    [:body
     [:header [:h1 title]]
     [:main (map blog-post posts)]
     [:footer "DodaTech Blog"]]]))

;; Usage
(blog-layout "My Blog"
  [{:title "Post 1" :author "Alice" :date "2024-01-15" :body "Hello!"}
   {:title "Post 2" :author "Bob" :date "2024-01-16" :body "World!"}])

What's Next

Now that you understand Hiccup, explore advanced testing techniques for Clojure web applications.

Topic Description Link
Clojure Advanced Testing Testing web applications {{< ref "24-testing-advanced" >}}
Clojure Lein Profiles Project configuration {{< ref "25-lein-profiles" >}}

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro