- ClayJS
- Guide
- Visual guide
Visual guide
The save lifecycle, the record, regions and the three-way merge, drawn and animated.
A visual guide to malleable documents
ClayJS is a script tag that makes a plain HTML file save its own edits back into itself. There is no separate database and no build step.
The core loop: saving the file
A malleable HTML file overwrites itself whenever it saves. Because whatever ends up in the saved file is the application, getting that saved output right is the whole job.
flowchart TB agent[text editor or AI agent] -.-> file["<b>File on disk</b>"] file -->|open| page["<b>Page in the browser</b><br>content + editing controls"] page -->|save| keep["<b>Saved with controls</b><br>everyone keeps editing"] page -->|save| done["<b>Saved as a finished piece</b><br>controls hidden from viewers"]
In a traditional web app, HTML is a disposable view rendered from a database. In a malleable document, the HTML file is the database. When a user edits text, checks a box, or reorders a list, those changes happen directly in the DOM, and saving writes the updated HTML document back over the file it came from.
Live sync
When you load ?plugins=sync, ClayJS handles live collaboration and external file edits automatically. Every accepted save from another browser tab, or any direct edit to the file on disk from a text editor or AI agent, merges into your open page in place through a three-way DOM merge without a reload.
You do not write merge logic or networking code. Your only job as the author is to mark temporary UI state so it does not get written into the saved file and broadcast to everyone else.
What gets saved
Every element in an open document is either durable record data, editing chrome, or a derived value computed from the record. ClayJS uses attributes on regions and elements to strip temporary state as an edit travels from your browser tab to other collaborators, to disk, and to viewers.
flowchart TB rec["Record HTML + persist"] --- edit edit["1 · Edit page"] -->|"<b>Stays in this tab</b><br>clay=#quot;editor-ui#quot;<br>ephemeral state<br>clay=#quot;freeze#quot; contents"| sync["2 · Sync"] sync -->|"<b>Stripped on save</b><br>no-save<br>editmode, savestatus"| saved["3 · Saved file"] saved -->|"<b>Hidden by CSS</b><br>show-when:editmode=#quot;true#quot;"| viewer["4 · Viewer"]
clay="editor-ui"marks runtime toolbars, filters, and tab-only counts (bundlingno-save,no-snapshot,no-watch,no-undo, andno-data). Clone it from a<template>atclay.readyso it stays in the current tab and is stripped before sync and save.ephemeral="selected"andephemeral:openmark per-tab UI state on a saved element. The element itself is saved, but the temporary token oropenattribute is ignored by sync and stripped on save.clay="freeze"turns a saved element's inner HTML into a per-tab scratchpad (the element saves and syncs, but its runtime contents never leave the tab), whileclay="no-dirty"marks derived totals that save into the file alongside real edits without triggering saves on their own.
Styles that stay in the file
clay.style("app-colors", css) refreshes a saved style in place. The old CSS paints first, then the script supplies the current CSS. This update does not count as an edit.
The saved file
<style data-name="app-colors"
clay="no-dirty">
.status { color: red; }
</style>
CSS is there before the script runs.
After the script runs
<style data-name="app-colors"
clay="no-dirty">
.status { color: blue; }
</style>
The same tag holds the current CSS.
The page stays clean
No unsaved changesNo undo step, autosave, or close warning from this update. The next real save carries the new CSS.
Use editor-ui for styles that must stay in one tab. See styles a script writes.
Editors and viewers
You might not need any of this. If you are building a document or tool that only you and your colleagues open to edit, every control can stay visible and active all the time.
Hiding controls
Put show-when:editmode="true" (with clay-options.js and a one-line CSS fallback rule) on buttons and drag handles that viewers should not see.
Editable text
Put editable on any block of text, such as <div editable>. Editors get a rich text editor with formatting, and viewers get the plain markup.
For plain text with no formatting, such as titles, labels or many small fields, use editmode:contenteditable and write it in the file as inert-contenteditable="plaintext-only".
Keeping form values
Put persist on checkboxes, inputs, and selects so ClayJS copies their live browser state into HTML attributes on save. Add viewmode:disabled disabled to lock them for viewers.
View mode only matters when you want to share the same HTML file with other people as a finished, stripped-down page. show-when and editable handle the difference between an editor and a viewer. Every document needs persist on form controls whose values should save.
Types of documents
Most malleable documents fall into one of two types, depending on whether everyone edits the file or some people only view it.
Editors-only document
For personal software or internal team tools where everyone who opens the file can edit it. All controls stay visible in the file as ordinary HTML.
Where the data lives: directly in HTML elements, attributes, child order, and persist form controls. The open page and the saved file are identical.
Editors-and-viewers document
For shared documents, task lists, or pages that you edit in place and publish to visitors as a clean read-only document.
Where the data lives: in HTML elements with data-id, editmode:contenteditable text, and persist controls authored in their saved inert form, alongside static controls marked show-when:editmode="true".
Building something more interactive?
An interactive editor
When your app has filter bars, search inputs, block gutters, or live summary counts that scripts update while you work.
Where the data lives: durable records stay in ordinary HTML elements with data-id. Interactive toolbars, filters, and tab-only counts live inside a clay="editor-ui" wrapper cloned from a <template> at clay.ready, and per-tab view state lives in the URL hash or ephemeral attributes.
A game or an app with its own data model
When HTML elements are a poor fit for the raw state, such as a chart, canvas, spreadsheet grid, or multiplayer game.
Where the data lives: in a <script type="application/json" id="model" merge="store"> tag (or with clay="live" and ?plugins=sync,live for sub-second sync on hyperclay.com and between tabs of one browser, checkpointed to disk every 8 seconds), while a render() function draws the view into a clay="editor-ui" mount.
Three rules that prevent drift
Follow these three rules so the file on disk, your open browser tab, and every collaborator or agent stay in step.
1. Most of the page is fine to save, but edit controls should stay out
Rows, cards and other records your code adds are real edits, and they should save. Toolbars, filters, selections and other runtime UI your code adds must be marked, or they save into the file and sync to everyone.
- Keep static edit buttons in the file with
show-when:editmode="true". - Clone script-created toolbars, filters, and tab-only counts from a
<template>into aclay="editor-ui"container afterclay.ready. - Use
clay.ephemeral.toggle(el, "selected")or a runtime stylesheet insideeditor-uifor tab-local state, rather than toggling plain classes orhiddenon records. - Without this, script-injected toolbars save into the file and duplicate on every open, and clicking a card selects or hides it for every collaborator.
Right: the script mounts its bar into clay="editor-ui"
Wrong: the script appends an unmarked bar
2. Adopt the DOM on load, and reconcile on change
The live DOM is the single source of truth, and live sync or undo can update it under your script at any time.
- Never rewrite the record at boot. Start your app with
clay.start(() => { … }), which runs it once even when live sync re-inserts the script. - Stamp a unique
data-idon every new or cloned item so sync can pair items across tabs. - Draw counts, totals and filtered views inside
clay.effect(render). It runs again after every edit, merge, undo and redo. Keep what it draws in editor UI orclay="no-dirty"; the console warns when it writes to the saved record. - Delegate click listeners on
document, so elements a merge re-creates keep working. - Without this, items created in two tabs without
data-idcan collide, and one may be silently dropped. A count drawn once at boot goes stale as soon as another tab changes the list.
Right: the count is drawn in clay.effect
Wrong: the count was drawn once at boot
3. Private documents are fine, as long as they stay private
A malleable document carries all of its data. Kept private, that data stays private. Shared or published, anyone can open View Source and read all of it.
- Hidden elements are still in the source:
hidden, CSS andshow-when:editmode="true"hide nothing from View Source. - Move anything confidential to its own file before you share the document.
Right: private data lives in a separate file
Wrong: hidden with CSS, still in the source
Where state goes
ClayJS saves the live DOM back to the file, so where you put state in the page decides whether it saves, syncs to other people or stays in your tab. Match each piece of your app to one of the five places below.
Records
Your saved content: list items, kanban cards, table rows and notes. Everyone sees them, and they save into the file.
- Put them directly in the HTML as ordinary elements, with a unique
data-idon each item so sync and undo can match them. - Add
persistto any checkbox, input or select whose value should save, andviewmode:disabled disabledso viewers cannot change it.
<li data-id="t-1">
<input type="checkbox" persist viewmode:disabled disabled>
<span editmode:contenteditable inert-contenteditable="plaintext-only">Task</span>
</li>
Edit controls
Add buttons, delete buttons, drag handles and other fixed controls that only editors use.
- Write them once in the HTML with
show-when:editmode="true"so viewers never see them. - Load
clay-options.jsand include its one-line CSS fallback rule so controls stay hidden before scripts run.
<button type="button" show-when:editmode="true">+ Add</button>
Complex editor controls and derived counts
Toolbars, menus, filter bars and live counts that your JavaScript builds or updates while the page is open.
- Clone script-built controls from a
<template>into aclay="editor-ui"element insideclay.start. That region is never saved, so runtime UI never piles up in the file. - If a computed total must be saved in the file, put it in
clay="no-dirty"so your script can recalculate it without ClayJS treating that update as a user edit. It saves alongside real edits and an incoming sync may replace it, but updating it never marks the page unsaved, autosaves or warns on close.
<!-- cloned in clay.start, never saved -->
<div clay="editor-ui">2 of 5 done</div>
<!-- saved in the file, but recalculating is not an edit -->
<output clay="no-dirty">$900.00</output>
Temporary toggles and popovers
Which item is selected, which popover or details box is open, or what is in the search box. This state belongs to one tab and should never save or sync.
- Mark temporary state on a saved element with
ephemeral="selected"orephemeral:openso it stays in your tab and never saves. - Keep a search query or open card ID in the URL hash, such as
#q=design, and apply it with a<style>tag insideclay="editor-ui".
<li class="card" data-id="c-1" ephemeral="selected">...</li>
<details ephemeral:open>...</details>
Data-rich apps and games
Applications with their own schema, like a chart model or spreadsheet grid, and real-time games that update faster than a file save.
- Put structured data in a JSON
<script>tag with amergename, such asmerge="game", and render the page from it. - Add
clay="live"with?plugins=sync,livefor sub-second sync on hyperclay.com and between tabs of one browser.
<script type="application/json" merge="game" clay="live">
{"turn": "x", "score": 0}
</script>
The smallest complete editors-and-viewers document
<!DOCTYPE html>
<html lang="en" autosave>
<head>
<meta charset="utf-8">
<title>Tasks</title>
<style>html:not([editmode="true"]) [show-when\:editmode="true"] { display: none }</style>
autosave on <html> saves 1.5 s after edits settle. The <style> rule hides show-when:editmode="true" controls from viewers before any script loads (the backslash escapes the colon in CSS, and :not([editmode="true"]) matches the saved file where editmode is absent).</head>
<body>
<h1 editmode:contenteditable inert-contenteditable="plaintext-only">Tasks</h1>
editmode:contenteditable activates editing in edit mode and saves as inert-contenteditable="plaintext-only" so viewers get plain text from the very first open. <ul id="tasks" sortable="tasks">
sortable="tasks" makes list items draggable in edit mode by their sortable-handle grip and saves the new child order as HTML. <li data-id="t-1"><span show-when:editmode="true" sortable-handle>↕</span> <input type="checkbox" persist viewmode:disabled disabled> <span editmode:contenteditable inert-contenteditable="plaintext-only">Write the brief</span></li>
</ul>
data-id so sync and undo can match it across versions. The checkbox uses persist so checking it writes a checked attribute into the file, and viewmode:disabled disabled so viewers cannot toggle it. <button show-when:editmode="true" type="button" add-from="#task-tpl" add-to="#tasks">Add task</button>
add-from and add-to require no JavaScript: clicking in edit mode clones #task-tpl into #tasks, stamps a fresh data-id on the new <li>, and focuses its first field, which here is the checkbox. <template id="task-tpl">
<li><span show-when:editmode="true" sortable-handle>↕</span> <input type="checkbox" persist viewmode:disabled> <span editmode:contenteditable contenteditable="plaintext-only">New task</span></li>
</template>
<template> are written in their active form (live contenteditable, no disabled) and carry no data-id, because they are cloned while editing and each copy receives a fresh id. <script src="https://clayjs.com/v1/clay.js?plugins=sync,sortable,indicator&exclude=richclay"></script>
<script src="https://clayjs.com/v1/clay-options.js"></script>
</body>
</html>
sync merges external and peer edits, sortable enables drag reordering, indicator shows the save status chip, and exclude=richclay skips the rich-text editor since every field here is plain text. clay-options.js compiles show-when:editmode="true" into CSS.