Building Browser-Native Presentations with Scittle
scittle, browser, presentations, hot-reload
About This Project
This presentation system was born out of a practical need while preparing technical talks for the Clojure community. Like many developers, I found myself building various tools and demos to showcase Clojure/ClojureScript concepts effectively. During this process, I discovered that Scittle’s browser-native approach could eliminate the typical build complexity that often holds people back from creating interactive presentations.
What started as a personal tool for my own presentations evolved into something I felt compelled to share. The Clojure community has given me so much through open-source libraries, helpful discussions, and shared knowledge. This project is my way of contributing back - offering a simple, accessible way for anyone to create beautiful, interactive presentations without fighting with build tools.
Whether you’re preparing for a conference talk, a meetup presentation, or just want to create interactive documentation, I hope these examples inspire you and save you the hours I spent figuring out the details. The source code is fully available for you to learn from, adapt, and improve upon.
Let’s dive into why this approach matters and how you can use it.
Building Browser-Native Presentations with Scittle
The Problem
You want to create a technical presentation with live code examples. Traditional ClojureScript requires: - shadow-cljs or Figwheel configuration - Build tool setup and dependencies - Webpack or bundler configuration - Minutes of compile time on every change - Complex deployment process
What if you could skip all that and just write ClojureScript in an HTML file? No build step. No configuration. Just pure browser-native code.
That’s Scittle.
What is Scittle?
Scittle is a ClojureScript interpreter that runs entirely in the browser. Created by Michiel Borkent (the genius behind Babashka), it brings the simplicity of scripting to ClojureScript development.
Think of it like this:
JavaScript has script tags that run immediately. Python has interactive notebooks. Clojure has the REPL. Scittle gives ClojureScript the same instant, no-build experience.
Write code, refresh browser, see results. No compilation. No bundling. Pure interpretation.
The Simplest Presentation
(ns scittle.presentations.minimal
(:require
[reagent.core :as r]
[reagent.dom :as rdom]))
(defonce current-slide (r/atom 0))
(def slides
[{:title "Welcome to Scittle"
:content "Build presentations without build tools"
:bg "linear-gradient(to bottom right, #3b82f6, #9333ea)"}
{:title "No Compilation Required"
:content "ClojureScript runs directly in the browser"
:bg "linear-gradient(to bottom right, #10b981, #14b8a6)"}
{:title "Full Reagent Power"
:content "Interactive components with React"
:bg "linear-gradient(to bottom right, #f97316, #dc2626)"}
{:title "Instant Feedback"
:content "Changes appear immediately"
:bg "linear-gradient(to bottom right, #ec4899, #f43f5e)"}])
(defn presentation
[]
(let [{:keys [title content bg]} (get slides @current-slide)
at-start? (zero? @current-slide)
at-end? (= @current-slide (dec (count slides)))]
[:div.flex.flex-col.h-full
;; Gradient content area
[:div.flex-1.flex.flex-col.justify-center.p-6.rounded-lg
{:style {:background bg}}
[:div.text-center.text-white
[:h1.text-4xl.font-bold.mb-3 title]
[:p.text-xl.opacity-90 content]]]
;; Navigation centered below
[:div.mt-4
[:div.text-center.mb-3
[:span.text-base.font-semibold
(str (inc @current-slide) " / " (count slides))]]
[:div.flex.gap-2.mx-auto {:style {:width "fit-content"}}
[:button.btn.btn-sm.btn-primary
{:on-click #(swap! current-slide dec)
:disabled at-start?
:class (when at-start? "btn-disabled")}
"← Previous"]
[:button.btn.btn-sm.btn-primary
{:on-click #(swap! current-slide inc)
:disabled at-end?
:class (when at-end? "btn-disabled")}
"Next →"]]]]))
(rdom/render [presentation] (js/document.getElementById "minimal-demo"))See It Live
(kind/hiccup
[:div#minimal-demo {:style {:min-height "500px"}}
[:script {:type "application/x-scittle"
:src "minimal.cljs"}]])How It Works
The flow is straightforward: 1. Load Scittle.js from a CDN 2. Load the Reagent plugin 3. Write ClojureScript in a script tag with type=“application/x-scittle” 4. Scittle interprets and executes the code 5. Reagent renders React components 6. Everything updates live in the browser!
Visually, the architecture looks like this:
(kind/mermaid
"flowchart LR
A[HTML] --> B[Scittle]
B --> C[Reagent]
C --> D[DOM]")Key insight: Scittle evaluates ClojureScript directly in the browser. No compilation. No bundling. Just interpretation.
Thumbnail Overview
A professional touch for your presentations - let your audience see the big picture. Thumbnails provide visual context and make navigation instant.
Why thumbnails matter: - Quick overview of all slides - Jump to any section instantly - See where you are in the presentation - Makes long presentations more navigable
(ns scittle.presentations.thumbnails
(:require
[reagent.core :as r]
[reagent.dom :as rdom]))
(defonce current-slide (r/atom 0))
(defonce show-thumbnails? (r/atom false))
(def slides
[{:title "Thumbnail Overview"
:content "Click 'Show Thumbnails' to see all slides at once"
:bg "linear-gradient(to bottom right, #3b82f6, #9333ea)"}
{:title "Quick Navigation"
:content "Jump to any slide instantly"
:bg "linear-gradient(to bottom right, #10b981, #14b8a6)"}
{:title "Visual Context"
:content "See where you are in the presentation"
:bg "linear-gradient(to bottom right, #f59e0b, #f97316)"}
{:title "Professional Touch"
:content "Makes presentations feel polished"
:bg "linear-gradient(to bottom right, #ef4444, #dc2626)"}
{:title "Easy to Build"
:content "Just render slides in a grid"
:bg "linear-gradient(to bottom right, #8b5cf6, #9333ea)"}
{:title "Scittle Powered"
:content "No build tools needed!"
:bg "linear-gradient(to bottom right, #ec4899, #f43f5e)"}])
(defn thumbnail-view []
[:div.fixed.inset-0.bg-black.bg-opacity-75.z-50.overflow-auto
{:on-click #(reset! show-thumbnails? false)}
[:div.container.mx-auto.p-8
[:div.grid.grid-cols-3.gap-4
(for [[idx slide] (map-indexed vector slides)]
^{:key idx}
[:div.cursor-pointer.transform.transition.hover:scale-105
{:on-click (fn [e]
(.stopPropagation e)
(reset! current-slide idx)
(reset! show-thumbnails? false))}
[:div.card.shadow-lg.rounded-lg
{:class (when (= idx @current-slide) "ring-4 ring-blue-500")
:style {:background (:bg slide)
:min-height "150px"}}
[:div.card-body.text-white.text-center
[:h3.text-lg.font-bold.mb-2 (:title slide)]
[:p.text-sm.opacity-90 (:content slide)]
[:div.text-xs.mt-2.opacity-75
(str "Slide " (inc idx) "/" (count slides))]]]])]]])
(defn presentation
[]
(let [{:keys [title content bg]} (get slides @current-slide)]
[:div
[:div.flex.flex-col.h-full
;; Gradient content area
[:div.flex-1.flex.flex-col.justify-center.p-6.rounded-lg
{:style {:background bg}}
[:div.text-center.text-white
[:h1.text-4xl.font-bold.mb-3 title]
[:p.text-xl.opacity-90 content]]]
;; Navigation section - all centered
[:div.mt-4
;; Show Thumbnails button - centered
[:div.mb-4.text-center
[:button.btn.btn-sm.btn-secondary
{:on-click #(reset! show-thumbnails? true)}
"📋 Show Thumbnails"]]
;; Centered page counter and navigation
[:div
[:div.text-center.mb-3
[:span.text-base.font-semibold
(str (inc @current-slide) " / " (count slides))]]
[:div.flex.gap-2.mx-auto {:style {:width "fit-content"}}
[:button.btn.btn-sm.btn-primary
{:on-click #(swap! current-slide dec)
:disabled (zero? @current-slide)
:class (when (zero? @current-slide) "btn-disabled")}
"←"]
[:button.btn.btn-sm.btn-primary
{:on-click #(swap! current-slide inc)
:disabled (= @current-slide (dec (count slides)))
:class (when (= @current-slide (dec (count slides))) "btn-disabled")}
"→"]]]]]
;; Thumbnail overlay
(when @show-thumbnails?
[thumbnail-view])]))
(rdom/render [presentation] (js/document.getElementById "thumbnails-demo"))Try the Thumbnail View
(kind/hiccup
[:div#thumbnails-demo {:style {:min-height "550px"}}
[:script {:type "application/x-scittle"
:src "thumbnails.cljs"}]])Live Code Editor
The killer feature: evaluating code directly in your presentation. Scittle provides scittle.core.eval_string - a function that evaluates ClojureScript code at runtime, in the browser.
Perfect for: - Interactive tutorials - Live coding demonstrations - Teaching ClojureScript - API documentation with runnable examples - Conference talks with audience participation
(ns scittle.presentations.code-editor
(:require
[reagent.core :as r]
[reagent.dom :as rdom]))
(defonce code-input (r/atom "(+ 1 2 3)"))
(defonce output (r/atom ""))
(defonce error-msg (r/atom nil))
(defn eval-code!
[]
(reset! error-msg nil)
(try
(let [result (js/scittle.core.eval_string @code-input)]
(reset! output (pr-str result)))
(catch js/Error e
(reset! error-msg (.-message e))
(reset! output ""))))
(defn code-editor
[]
[:div.container.mx-auto.p-8
[:div.card.shadow-lg
[:div.card-body
[:h2.text-2xl.font-bold.mb-4 "Live ClojureScript Editor"]
[:p.text-gray-600.mb-4
"Type ClojureScript code and click 'Evaluate' to see the result"]
;; Code input
[:div.mb-4
[:label.block.text-sm.font-semibold.mb-2 "Code:"]
[:textarea.form-control.font-mono.text-sm
{:value @code-input
:on-change #(reset! code-input (-> % .-target .-value))
:rows 6
:placeholder "Enter ClojureScript code..."}]]
;; Evaluate button
[:button.btn.btn-primary.mb-4
{:on-click eval-code!}
"▶ Evaluate"]
;; Output
[:div
[:label.block.text-sm.font-semibold.mb-2 "Result:"]
(if @error-msg
[:div.alert.alert-error
[:strong "Error: "] @error-msg]
[:pre.bg-gray-100.p-4.rounded.overflow-x-auto
[:code.text-sm @output]])]
;; Examples
[:div.mt-6
[:p.text-sm.font-semibold.mb-2 "Try these examples:"]
[:div.flex.flex-wrap.gap-2
[:button.btn.btn-sm.btn-outline
{:on-click #(reset! code-input "(+ 1 2 3 4 5)")}
"Addition"]
[:button.btn.btn-sm.btn-outline
{:on-click #(reset! code-input "(map inc [1 2 3 4])")}
"Map"]
[:button.btn.btn-sm.btn-outline
{:on-click #(reset! code-input "(filter even? (range 10))")}
"Filter"]
[:button.btn.btn-sm.btn-outline
{:on-click #(reset! code-input "(reduce + (range 1 11))")}
"Reduce"]
[:button.btn.btn-sm.btn-outline
{:on-click #(reset! code-input "(defn greet [name]\n (str \"Hello, \" name \"!\"))\n\n(greet \"Scittle\")")}
"Function"]]]]]
[:div.card.shadow-lg.mt-6
[:div.card-body
[:h3.text-lg.font-bold.mb-2 "How It Works"]
[:ul.list-disc.list-inside.space-y-2.text-gray-700
[:li "Scittle provides " [:code.bg-gray-100.px-1 "scittle.core.eval_string"]]
[:li "Evaluates ClojureScript code in the browser"]
[:li "No compilation step - instant feedback"]
[:li "Perfect for interactive documentation"]
[:li "Great for teaching and demonstrations"]]]]])
(rdom/render [code-editor] (js/document.getElementById "code-editor-demo"))Try It Yourself
Type any ClojureScript code and click “Evaluate” to see the result!
(kind/hiccup
[:div#code-editor-demo {:style {:min-height "700px"}}
[:script {:type "application/x-scittle"
:src "code_editor.cljs"}]])Shadow-cljs vs Scittle: Choosing the Right Tool
Both are excellent tools, but they serve different purposes. Choose based on your project’s needs:
(kind/hiccup
[:div.overflow-x-auto
[:table.table.table-zebra.w-full
[:thead
[:tr
[:th "Feature"]
[:th "Shadow-cljs"]
[:th "Scittle"]]]
[:tbody
[:tr
[:td [:strong "Compilation"]]
[:td "Ahead-of-time (AOT)"]
[:td "Just-in-time (JIT)"]]
[:tr
[:td [:strong "Setup Required"]]
[:td "Yes - deps.edn, config"]
[:td "No - just HTML + CDN"]]
[:tr
[:td [:strong "Build Time"]]
[:td "Seconds to minutes"]
[:td "Instant (no build)"]]
[:tr
[:td [:strong "Bundle Size"]]
[:td "Optimized, tree-shaken"]
[:td "~1MB runtime + code"]]
[:tr
[:td [:strong "Performance"]]
[:td "Fastest (compiled)"]
[:td "Fast (interpreted)"]]
[:tr
[:td [:strong "Hot Reload"]]
[:td "Yes (via tooling)"]
[:td "Just refresh browser"]]
[:tr
[:td [:strong "npm Integration"]]
[:td "Full support"]
[:td "Limited (CDN only)"]]
[:tr
[:td [:strong "Code Splitting"]]
[:td "Yes"]
[:td "Manual only"]]
[:tr
[:td [:strong "Best For"]]
[:td "Production apps, SPAs"]
[:td "Prototypes, demos, docs"]]
[:tr
[:td [:strong "Learning Curve"]]
[:td "Moderate"]
[:td "Very low"]]
[:tr
[:td [:strong "Deployment"]]
[:td "Build + host static files"]
[:td "Copy HTML file anywhere"]]]]])| Feature | Shadow-cljs | Scittle |
|---|---|---|
| Compilation | Ahead-of-time (AOT) | Just-in-time (JIT) |
| Setup Required | Yes - deps.edn, config | No - just HTML + CDN |
| Build Time | Seconds to minutes | Instant (no build) |
| Bundle Size | Optimized, tree-shaken | ~1MB runtime + code |
| Performance | Fastest (compiled) | Fast (interpreted) |
| Hot Reload | Yes (via tooling) | Just refresh browser |
| npm Integration | Full support | Limited (CDN only) |
| Code Splitting | Yes | Manual only |
| Best For | Production apps, SPAs | Prototypes, demos, docs |
| Learning Curve | Moderate | Very low |
| Deployment | Build + host static files | Copy HTML file anywhere |
When to Use Scittle
Perfect for: - Technical presentations with live demos - Interactive documentation - Quick prototypes and experiments - Teaching ClojureScript to beginners - Shareable code examples (CodePen style) - Single-file applications
When to Use Shadow-cljs
Better for: - Production web applications - Large codebases with many dependencies - Performance-critical applications - Team projects requiring build tooling - Apps needing npm package integration - Progressive web apps (PWAs)
Best Practices & Tips
Structure Your Code
Keep your Scittle scripts organized and modular:
(kind/code
";; Good: Separate concerns
(ns my-app.core)
(defn render-slide [idx]
...)
(defn navigation-buttons []
...)
(defn main []
(render-slide 0)
(setup-keyboard-navigation!))");; Good: Separate concerns
(ns my-app.core)
(defn render-slide [idx]
...)
(defn navigation-buttons []
...)
(defn main []
(render-slide 0)
(setup-keyboard-navigation!))Leverage CDN Libraries
Scittle has plugins for popular libraries: - Reagent - React wrapper (scittle.reagent) - Re-frame - State management (scittle.re-frame) - Promesa - Promises (scittle.promesa) - ClojureScript.test - Testing (scittle.cljs-test)
Use Browser DevTools
Scittle code is debuggable in Chrome/Firefox DevTools: - Set breakpoints in your ClojureScript code - Inspect atoms and state in the console - Use js/console.log for quick debugging - Check Network tab to ensure scripts load correctly
Performance Considerations
Keep it snappy: - Minimize initial script size (under 100KB recommended) - Use lazy loading for heavy computations - Cache expensive calculations in atoms - Consider memoization for pure functions - Test on slower devices/connections
Common Gotchas & Solutions
1. Namespace Loading Order
Problem: Trying to use a namespace before it’s loaded.
(kind/code
";; ❌ Won't work
(ns my-app.core
(:require [my-app.utils :as utils]))
;; ✅ External file must load first
;; In HTML:
;; <script type=\"application/x-scittle\" src=\"utils.cljs\"></script>
;; <script type=\"application/x-scittle\" src=\"core.cljs\"></script>");; ❌ Won't work
(ns my-app.core
(:require [my-app.utils :as utils]))
;; ✅ External file must load first
;; In HTML:
;; <script type="application/x-scittle" src="utils.cljs"></script>
;; <script type="application/x-scittle" src="core.cljs"></script>2. Async Operations
Problem: Scittle scripts load asynchronously.
(kind/code
";; ❌ Might fail
(js/document.addEventListener \"DOMContentLoaded\"
(fn [] (start-app!)))
;; ✅ Better: Use window load event
(js/window.addEventListener \"load\"
(fn [] (start-app!)))");; ❌ Might fail
(js/document.addEventListener "DOMContentLoaded"
(fn [] (start-app!)))
;; ✅ Better: Use window load event
(js/window.addEventListener "load"
(fn [] (start-app!)))3. CORS Issues
Problem: Loading scripts from different domains.
Solution: Either: - Host all files on the same domain - Use a CORS proxy for development - Configure server to allow CORS headers
4. Not All npm Packages Work
Scittle can’t use npm packages directly. Workarounds: - Use CDN versions (unpkg.com, jsdelivr.com) - Look for Scittle-compatible plugins - Use browser-native APIs instead
5. Error Messages
Scittle error messages can be cryptic. Tips: - Check browser console for stack traces - Use (js/console.log) liberally during development - Test small code snippets in isolation - Verify syntax with a Clojure linter
Resources & Next Steps
Official Documentation
- Scittle Repository: github.com/babashka/scittle
- Scittle Plugins: Available for Reagent, Re-frame, Promesa, and more
- Babashka Book: book.babashka.org - Covers Scittle usage
- ClojureScript: clojurescript.org
Example Projects
- Maria.cloud: Interactive ClojureScript learning platform
- 4Clojure: Problem-solving platform using browser ClojureScript
- Klipse: Interactive code snippets for blogs
- This Presentation!: Available on GitHub
Community & Support
- Clojurians Slack: #scittle and #clojurescript channels
- ClojureVerse: Community forum for discussions
- r/Clojure: Reddit community
- Clojure Mailing List: Active community discussions
Try It Today!
Start building your first Scittle presentation:
(kind/code
"<!DOCTYPE html>
<html>
<head>
<script src=\"https://cdn.jsdelivr.net/npm/scittle@0.6.15/dist/scittle.js\"></script>
<script src=\"https://cdn.jsdelivr.net/npm/scittle@0.6.15/dist/scittle.reagent.js\"></script>
</head>
<body>
<div id=\"app\"></div>
<script type=\"application/x-scittle\">
(ns my-app.core
(:require [reagent.core :as r]
[reagent.dom :as rdom]))
(defn app []
[:div [:h1 \"Hello from Scittle!\"]])
(rdom/render [app] (js/document.getElementById \"app\"))
</script>
</body>
</html>")<!DOCTYPE html>
<html>
<head>
<script src="https://cdn.jsdelivr.net/npm/scittle@0.6.15/dist/scittle.js"></script>
<script src="https://cdn.jsdelivr.net/npm/scittle@0.6.15/dist/scittle.reagent.js"></script>
</head>
<body>
<div id="app"></div>
<script type="application/x-scittle">
(ns my-app.core
(:require [reagent.core :as r]
[reagent.dom :as rdom]))
(defn app []
[:div [:h1 "Hello from Scittle!"]])
(rdom/render [app] (js/document.getElementById "app"))
</script>
</body>
</html>Conclusion
Scittle brings the joy back to ClojureScript development.
No more waiting for builds. No more configuration headaches. Just open an HTML file and start coding.
Perfect for: - Learning ClojureScript - Creating interactive presentations - Building quick prototypes - Teaching and documentation
Remember: - Use Shadow-cljs for production applications - Use Scittle for everything else
Ready to build browser-native presentations? Fork this presentation and make it your own!
Thank you! Questions? Find me on Clojurians Slack.
Built with Scittle, Reagent, and ❤️ for functional programming