A HTML-first framework. A drop-in bundle of Alpine, HTMX, Hyperscript, and PicoCSS for building modern, reactive applications.
- HTML 47.5%
- Clojure 37.1%
- CSS 10%
- JavaScript 5.4%
| .clj-kondo | ||
| coverage | ||
| pages | ||
| script | ||
| src | ||
| test-results | ||
| tests | ||
| .gitignore | ||
| AGENTS.md | ||
| bb.edn | ||
| biome.json | ||
| index.html | ||
| LICENSE | ||
| mise.toml | ||
| package.json | ||
| playwright.config.js | ||
| pnpm-lock.yaml | ||
| README.md | ||
| squint.edn | ||
| vite.config.js | ||
Solo
HTML-first reactive apps. JS expressions. Automatic persistence.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="stylesheet" href="solo.css">
</head>
<body>
<main class="container" let="counter = 0" save="my-app">
<p>Count: <span text="counter"></span></p>
<button on:click="set('counter', c => (c ?? 0) + 1)">+1</button>
</main>
<script type="module">
import { mount } from './solo.js'
mount({})
</script>
</body>
</html>
Install
npm install solo-js
Or use directly from CDN (no install):
<link rel="stylesheet" href="https://esm.sh/solo-js/solo.css">
<script type="module">
import { mount } from 'https://esm.sh/solo-js?standalone'
mount({})
</script>
Quick Start
Counter (Reactivity)
<main let="count = 0">
<p text="count"></p>
<button on:click="set('count', c => c + 1)">+1</button>
</main>
Todo List (Tables + Each)
<main let="text = ''">
<input model="text" placeholder="New todo">
<!-- ins() writes to the 'todos' table (separate from values) -->
<button on:click="ins('todos', {text, done: false}); set('text', '')">Add</button>
<!-- each reads from the 'todos' table automatically -->
<ul each="todo of todos">
<template>
<li>
<input type="checkbox" :checked="todo.done" on:change="upd(todo, 'done', checked)">
<span text="todo.text" class:muted="todo.done"></span>
<button on:click="del(todo)">×</button>
</li>
</template>
</ul>
</main>
Persistence
<main let="notes = ''" save="my-notes-app">
<textarea model="notes" placeholder="Your notes..."></textarea>
</main>
The save attribute persists all state to IndexedDB automatically.
Documentation
Full documentation: Run the docs site locally with mise run dev and open http://localhost:5173
- API Reference — Complete directives, helpers, and JavaScript API
- Themes & CSS — Design variants, dark mode, utility classes
- Fragments — Static includes and hash-based routing
Directives
| Directive | Purpose | Example |
|---|---|---|
let |
Declare values | let="counter = 0" |
save |
Auto-persist to IndexedDB | save="my-app" |
text |
Bind text content | text="name" |
show |
Conditional visibility | show="count > 0" |
transition |
Animate on show | transition="fade" |
model |
Two-way input binding | model="name" |
each |
Iterate arrays/tables | each="todo of todos" |
case/of |
View switching | case="view" + of="home" |
:attr |
Bind any attribute | :disabled="loading" |
class:name |
Conditional class | class:active="selected" |
on:event |
Event handler | on:click="set('x', 1)" |
load |
Static fragment include | load="header.html" |
load-route |
Hash-based routing | load-route="pages/" |
nav-sync |
Sync active nav link | <nav nav-sync> |
See the API Reference for full details.
Core Helpers
set('key', value) // Set a value
set('counter', c => c + 1) // Update with function
ins('todos', {text, done: false}) // Insert row into table
upd(todo, 'done', true) // Update row field
del(todo) // Delete row
batch(() => { ... }) // Batch mutations (single render)
JavaScript API
import { mount, subscribe, batch, register } from 'solo-js'
// Initialize
await mount({ debug: true })
// Watch state
const unsub = subscribe(['count'], ({ count }) => console.log(count))
// Custom functions
register('double', n => n * 2)
Browser Support
Solo targets modern evergreen browsers (Chrome, Firefox, Safari, Edge). Requires:
- ES2017+ (
Proxy,async/await) fetchand IndexedDBunsafe-evalCSP (usesnew Function()for expressions)
IE is not supported. In environments without IndexedDB, Solo runs memory-only.
Bundle Size
~34KB JS (8KB gzip) + ~24KB CSS (5KB gzip). No runtime dependencies.
Examples
See tests/apps/ for working examples:
kitchen-sink.html— Full app with sidebar, routingfeatures/counter.html— Basic reactivityfeatures/todo.html— CRUD with searchfeatures/forms.html— Validation patterns
Development
pnpm install
mise run build # → dist/solo.js + dist/solo.css
mise run dev # Dev server + test apps
mise run test # Run all tests
License
Apache 2.0