Tutuca
A small JavaScript framework for interactive web apps.
Typed state, built-in testing tools, and pluggable sync. Small enough to read in full.
Define your components, their state, and the messages they handle. Tutuca uses those definitions to render your UI, check for mistakes, and generate test cases. Explore components in the built-in storybook, then add a sync engine to save state or share changes across tabs and devices.
Start in the playground or import Tutuca into your own page. No build step required.
- Write components that do one thing and do it well
- Write components to work together
- Write components to handle messages, because that is the universal interface
Components to me means only messaging, local retention and protection and hiding of state-process, and extreme late-binding of all things
Get started
npm install tutuca
Or import directly from a CDN:
import { component, h, Host } from "https://cdn.jsdelivr.net/npm/tutuca/dist/tutuca.min.js";
A first component
This counter introduces the four parts of a component: a name, typed fields, a view, and message handlers. Try the buttons, then edit the code and press Ctrl+Enter to run your changes.
Open Steps to replay a test sequence, Stress test to try random messages, or Dev to inspect the state, component definitions, and lint results.
const host = new Host({ document, rootNode: document.getElementById("app") });
host.scope.register({ Counter });
host.start(host.scope.components.Counter());
To mount the counter on your own page, register it with a host and start it in an element with id app.
Build with components and messages
-
Components with typed state
Describe your state with schemas for strings, numbers, enums, optional values, arrays, maps, records, unions, and nested components.
-
Views as data
h,each,render,$ifandscopebuild a tree the framework can inspect, lint and optimize. -
Explicit message routing
Events send typed messages along explicit paths to the component that handles them.
-
Built-in drag and drop
Mark elements
draggable="true"and handleon:drop; the handler gets the dragged item's key, no library needed. See the example. -
Immutable updates
Write updates by editing
next, a draft of the new state. Tutuca keeps the previous one for the storybook timeline and sync comparisons. -
Efficient rendering
A small virtual DOM matches elements by key and reuses static parts of the view.
Save and share state
-
A diff guided by your schemas
Tutuca uses your field types to describe changes as small operations: set a field, insert a list item, or edit part of a string. Adding an item at the start of a list produces one insert operation.
-
Pluggable sync
Connect an engine to save changes and apply updates from elsewhere. Keep the same message handlers, and choose which fields are shared, saved only on this device, or kept only for the current session.
-
Three engines included
Use browser storage for persistence, a remote engine to exchange changes through a server, or Automerge to merge concurrent edits.
-
Time travel
Step back and forward through your app's states with the bar under each demo's header, or with Alt+β/β. Each step is one user action, with its async replies folded in. Time travel is itself a sync engine: going back applies the past state as a change from elsewhere, so storage and peers follow.
-
Repeatable handlers
Read time, random numbers, and IDs through
ctx. Tests can control these values to replay a run; apps use UUIDs to avoid ID collisions between peers. -
Hot code reloading
Edit a component and keep its state. Tutuca rebuilds the state with the new code: new fields take their defaults, and values that no longer fit are reset and reported along with broken invariants. Try it in the playground.
Try the notes demo: reload it to check persistence, or open the Worker or Automerge version in two tabs to see changes sync, or the Automerge relay version in two browsers. For Automerge, copy the full URL after the demo loads so both open the same document.
- Browser storage: saves state in localStorage across reloads
- Worker: simulates a server with a SharedWorker and saves state in IndexedDB
- Automerge: tabs sync over a BroadcastChannel, with no server
- Automerge relay: browsers and devices sync through the public Automerge relay; anyone with the URL can see the document
All demos support these ?sync= options, plus ?history=N to cap
the time travel timeline (0 turns it off).
Read the sync guide
for setup, field settings, and how each engine handles concurrent edits.
Test as you build
-
Headless testing
A driver runs messages through the real task loop with or without a DOM, and reports failed tasks and invalid state.
-
Property-based tests from your schemas
Generate random states, messages, and DOM events from your field and message types to exercise cases you might not think to write by hand.
-
Storybook built in
Write stories with test sequences, step through state changes on a timeline, and replay failing cases found by stress tests.
-
Lint and invariants
Find unknown fields, views, components, and handlers before running your app. In development mode, Tutuca also checks message arguments, state, and invariants as your app runs.
Understand how it works
-
A formal specification
A PLT Redex model of views, rendering, reconciliation, paths and the task queue, typeset as a PDF. in progress
-
DevTools formatters
Inspect components, paths, and tasks with readable formatting in the browser console.
-
A core you can read
About 2,000 lines of JavaScript in the core, with no npm dependencies. Includes a bundled copy of Immer.
-
Readable by coding agents
A compact codebase lets coding agents inspect the implementation while working on your app.
Learn by example
Edit and run each example as you go, from lists and forms to nested components and async requests. Explore more in the playground, including the component libraries.
Lists and forms
Build a todo list from an array of records. Render items with each,
read input values with targetValue(), and use getters for derived values
in the view.
Components inside components
Build a cart with a component for each item. Each item handles its own messages and
uses notify to request changes from the cart. Typed message arguments
enable runtime checks in development and generated stress tests.
Talking to the outside world
Use ask to delegate a request to a named handler and receive the result
as a message. Stories can substitute test handlers or send replies directly, so you
can test loading, success, and failure states.
Web components and custom events
Use custom elements as tags in a view, and handle the custom events they fire with
on: and the event's name. This picker comes from a CDN; its
emoji-click event bubbles out of its shadow root, and the handler reads
the chosen emoji from ctx.event.detail.
Drag and drop
Make an element draggable with draggable="true", and a drop target with
on:drop. In the drop's message, dragKey() is the dragged item
and sourceKey() the one it landed on. While you drag, Tutuca marks the
dragged element with data-dragging and the target under it with
data-draggingover, so styles can show where it will land. Filter the list,
then drag: the keys stay those of the full list.
When things go wrong
This component has three deliberate mistakes. Open Dev to see what the linter found, fix the code, and run it again.
Demos
Explore small apps built with the component libraries. Most also have stories you can inspect and test in the storybook. Run any demo on its own or try it with a sync engine.
-
Todo β
Projects, labels, priorities, subtasks and quick add that reads βtomorrow p1 #Workβ.
-
Thing tracker β
Log habits, moods and drinks with a tap; see insights by day, week or month.
Storybook Source: thing-tracker
-
Payload β Schema β
Edit an example API payload and get a documented JSON Schema for it.
Storybook Source: json-schema json
-
Sketchpad β
Draw rectangles, ellipses, lines and polylines; move, layer, undo and save as SVG.
-
Kanban β
Drag cards within a column or across columns; add and remove them as you go.
-
Algebra workbench β
Solve quadratics with their plot, invert 2Γ2 matrices and keep the problems you saved.
-
Notebook β
A notebook of the other demos' components: add notes, todos, boards, drawings, trackers and notebooks as cells, drag them around, collapse them.
Storybook Source: composition
-
AST notations β
A small language's syntax tree as components: read or edit it as Lisp, Python, JavaScript or Forth, and see what it evaluates to.
-
Tree spec explorer β
Step through the PLT Redex model of a component tree: pick which event or response comes next and watch where each message's address leads.
-
Notes β
A notes list for trying sync: the title and notes are shared, the filter stays on this device, the new note's text isn't kept.
Source: demos/notes
The runtime also runs as WebAssembly: tutuca-wax is tutuca's runtime written in Wax and compiled to wasm-gc, with components that are wasm modules of their own, loaded at run time and styled with Tailwind and margaui CSS compiled in the page. Its playground compiles components written in TCD and Wax in the browser and shows them running.
Project status
Beta parts work and are used by the demos, but their APIs may still change. Experimental parts are still being designed and may change a lot.
| Submodule | What it does | Status |
|---|---|---|
core | Components, views, rendering, and the task loop | beta |
storybook | Stories, test sequences, and the state timeline | beta |
sync | Schema-guided diffs and the sync engines | experimental |
dev | DevTools formatters and the inspector panel | beta |
playground | The editable, runnable playground element | beta |
test | Headless driver and property-based test generators | beta |
lint | Checks views and components for mistakes | beta |
embed | The <tutuca-app> custom element | beta |
css | Lists class names for CSS compilers | beta |