clay.js
  1. ClayJS
  2. Guide
  3. 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.

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.

  • clay="editor-ui" marks runtime toolbars, filters, and tab-only counts (bundling no-save, no-snapshot, no-watch, no-undo, and no-data). Clone it from a <template> at clay.ready so it stays in the current tab and is stripped before sync and save.
  • ephemeral="selected" and ephemeral:open mark per-tab UI state on a saved element. The element itself is saved, but the temporary token or open attribute 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), while clay="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 changes

No 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.

show-when:editmode="true" hides editing buttons from viewers while keeping them in the file for editors. show-when:editmode="true" Editor edit mode Buy milk + Add Viewer view mode Buy milk

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.

In edit mode an editable element is a rich text editor; viewers get a plain div. Editor B I U Weekly notes rich text editor Viewer Weekly notes plain div

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

In an editors-only document, controls and data live together in the saved HTML with nothing hidden or stripped.
Open page
Controls
Record HTML
Saved file
Controls
Record HTML

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

One saved file serves editors and viewers: viewers get the controls hidden by CSS and the records read-only. Editor Controls Record HTML editable Viewer Hidden Record HTML read-only

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

Editing chrome mounted at runtime in clay="editor-ui" is left out on save, so the saved file holds only the record HTML and its inert template. Open page editor-ui Record HTML <template> Save Saved file Record HTML <template>

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

The state lives in a mergeable JSON script tag; render() draws the view into clay="editor-ui", and only the JSON is saved. Open page JSON data merge="store" render() editor-ui Save Saved file JSON data merge="store"

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 a clay="editor-ui" container after clay.ready.
  • Use clay.ephemeral.toggle(el, "selected") or a runtime stylesheet inside editor-ui for tab-local state, rather than toggling plain classes or hidden on 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.

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-id on 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 or clay="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-id can collide, and one may be silently dropped. A count drawn once at boot goes stale as soon as another tab changes the list.

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 and show-when:editmode="true" hide nothing from View Source.
  • Move anything confidential to its own file before you share the document.

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-id on each item so sync and undo can match them.
  • Add persist to any checkbox, input or select whose value should save, and viewmode:disabled disabled so 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.js and 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 a clay="editor-ui" element inside clay.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" or ephemeral:open so 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 inside clay="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 a merge name, such as merge="game", and render the page from it.
  • Add clay="live" with ?plugins=sync,live for 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>
Each item has a unique 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>
Blueprints inside <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.

Part of the ClayJS complete guideDownload the whole guide as Markdown