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%
Find a file
2025-12-23 06:20:24 +08:00
.clj-kondo setup clj-kondo 2025-12-23 06:20:24 +08:00
coverage working on icons 2025-12-19 04:57:43 +08:00
pages setup clj-kondo 2025-12-23 06:20:24 +08:00
script fragment and doc fixes 2025-12-22 05:41:02 +08:00
src setup clj-kondo 2025-12-23 06:20:24 +08:00
test-results ergonomics 2025-12-22 14:35:59 +08:00
tests ergonomics 2025-12-22 14:35:59 +08:00
.gitignore setup clj-kondo 2025-12-23 06:20:24 +08:00
AGENTS.md setup clj-kondo 2025-12-23 06:20:24 +08:00
bb.edn setup clj-kondo 2025-12-23 06:20:24 +08:00
biome.json lots of docs and css fixes 2025-12-21 23:13:47 +08:00
index.html lots of docs and css fixes 2025-12-21 23:13:47 +08:00
LICENSE alpine test 2025-12-18 01:10:43 +00:00
mise.toml setup clj-kondo 2025-12-23 06:20:24 +08:00
package.json fragment and doc fixes 2025-12-22 05:41:02 +08:00
playwright.config.js fragment and doc fixes 2025-12-22 05:41:02 +08:00
pnpm-lock.yaml fragment and doc fixes 2025-12-22 05:41:02 +08:00
README.md fragment and doc fixes 2025-12-22 05:41:02 +08:00
squint.edn major shift to clojure 2025-12-22 04:36:48 +08:00
vite.config.js fragment and doc fixes 2025-12-22 05:41:02 +08:00

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)
  • fetch and IndexedDB
  • unsafe-eval CSP (uses new 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, routing
  • features/counter.html — Basic reactivity
  • features/todo.html — CRUD with search
  • features/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