Your data is a URL. LOSOS is a 6KB reactive framework for building apps on linked data. Edit a value — it auto-saves via HTTP PUT. Open the same URL on another device — it live-syncs via WebSocket. No API layer. No state management. The URL is the API.
5.9 KB total · zero dependencies · read the docs →
No JSX. No compiler. No VDOM. Tagged template literals with surgical DOM patching.
Add tasks in either panel — both sync instantly via CRDT. No server needed.
import { createStore, html, render, keyed } from './losos.js' var data = { '@id': '#this', '@type': 'Tracker', 'title': 'My Tasks', 'issue': [ { '@id': '#1', 'summary': 'Try editing this text', 'status': 'NEEDS-ACTION' }, { '@id': '#2', 'summary': 'Click the circle to complete', 'status': 'NEEDS-ACTION' }, { '@id': '#3', 'summary': 'Add a task below', 'status': 'NEEDS-ACTION' }, { '@id': '#4', 'summary': 'This is reactive linked data', 'status': 'COMPLETED' } ] } var store = createStore(data, { debounce: 500 }) var root = store.get('#this') var el = document.getElementById('live-demo') var now = new Date().toLocaleTimeString() function isDone(i) { return i['status'] === 'COMPLETED' } function renderDemo() { var issues = store.propAll(root, 'issue') var active = issues.filter(i => !isDone(i)) var done = issues.filter(isDone) render(el, html` <h2>${data['title']}</h2> <p>${active.length} tasks remaining · ${now}</p> <input placeholder="Add a task..." onkeydown="${addTask}" /> ${keyed(active, i => i['@id'], i => html` <div> <span onclick="${() => store.set(i, 'status', 'COMPLETED')}">○</span> ${i['summary']} </div> `)} ${done.length ? html` <div>COMPLETED</div> ${keyed(done, i => i['@id'], i => html` <div> <span onclick="${() => store.set(i, 'status', 'NEEDS-ACTION')}">✓</span> ${i['summary']} </div> `)} ` : null} `) } store.onChange(renderDemo) renderDemo() setInterval(() => { now = new Date().toLocaleTimeString(); renderDemo() }, 1000)
{
"@context": {
"ical": "http://www.w3.org/2002/12/cal/ical#",
"wf": "http://www.w3.org/2005/01/wf/flow#",
"dct": "http://purl.org/dc/terms/",
"title": "dct:title",
"summary": "ical:summary",
"status": "ical:status",
"issue": "wf:issue",
"Tracker": "wf:Tracker",
"Vtodo": "ical:Vtodo"
},
"@id": "#this",
"@type": "Tracker",
"title": "My Tasks",
"issue": []
}
// Looks like JSON. Is linked data.
// @context maps flat keys to standard URIs.
// Every node has an @id — a global, dereferenceable ref.<!-- The entire app shell --> <script type="application/ld+json" src="data.jsonld"></script> <script type="module" data-pane src="panes/my-pane.js"></script> <script src="https://unpkg.com/xlogin"></script> <div id="losos"></div> <script type="module" src="losos/shell.js"> </script>
store.set() triggers debounced PUT and notifies listeners${} holes patch.keyed(items, keyFn, templateFn) reuses DOM by key@id URIs for data (global, dereferenceable), ref() for DOM elementsical:Vtodo (RFC 5545). Not a proprietary format.onUnmount(container, fn) for cleanup on tab switch$schema to your data, get a free auto-generated edit form. Learn moreresolvePane and registry. Learn moreLOSOS includes persistence, auth, and live sync. Others don't.
| LOSOS | Preact | Svelte | Solid.js | lit-html | |
|---|---|---|---|---|---|
| Size (gzip) | 5.9 KB | 4.5 KB | ~2 KB* | 7 KB | 3.5 KB |
| Build step | No | JSX | Yes | JSX | No |
| Auto-persist | Built in | — | — | — | — |
| Live sync | WebSocket | — | — | — | — |
| Auth | xlogin | — | — | — | — |
| Linked Data | JSON-LD | — | — | — | — |
| Keyed lists | Yes | Yes | Yes | Yes | Yes |
| Shadow DOM | No | No | No | No | Required |
* Svelte runtime is ~2 KB but requires a compiler. Total shipped JS per app is higher.
JSON-LD with flat context aliases. Reads like plain JSON. @context maps keys to standard URIs. No @graph. Every node has an @id — a global, dereferenceable ref.
createStore(data, { url, authFetch, debounce }) gives you a reactive JSON-LD graph. Every set, push, remove call marks dirty, schedules a debounced PUT, and notifies listeners. On first save, the store discovers WebSocket support via the Updates-Via header and subscribes automatically.
Tagged template literals. First render builds DOM. Subsequent renders with the same template shape patch only the ${} holes. Event handlers via onclick="${fn}". Conditional rendering via ${condition ? html`...` : null}.
An ES module with canHandle(subject, store) and render(subject, store, container). The shell loads all <script data-pane> tags, checks canHandle against the data’s @type, builds a tab bar, and renders the first match. Add more panes by adding more script tags.
Every JSON-LD node has a URI. Navigate to a URI, the shell loads the data, finds matching panes, renders. The @id is the route. The @type picks the view. No router library needed — linked data already solved routing.
Data-first. Reactive. Tiny.
Examples Docs Try the demo LLM Skill