Reference
The core API, the attributes, the events, and the one endpoint that makes it all work. For the guided version, start with the tutorial.
The script tag
<script src="https://clayjs.com/clay.js"></script>
The default build includes the save lifecycle and rich text (editable). Plugins load conditionally through the URL, in the browser only:
| Param | Meaning | Example |
|---|---|---|
plugins | Comma-separated plugins to load | clay.js?plugins=sync,undo |
exclude | Remove a default-on plugin | clay.js?exclude=richclay |
Checkbox plugins: richclay (default on), indicator, sync, sortable, undo, cms. The configurator composes this URL for you. Everything else is a separate library with its own script tag: see Advanced and Plugins. npm builds for bundlers live at @panphora/clayjs.
window.clay
| Member | What it does |
|---|---|
clay.ready | A Promise that resolves once clayjs has booted (core loaded, plugins attached). In inline scripts, await clay.ready before touching anything else. clay:ready fires on document at the same moment. |
clay.save() | Snapshot the page and save it. Skips when nothing changed. Returns Promise<{msg, msgType}>; msgType is success, error, or skipped. Never rejects. |
clay.save.force() | Save even when nothing appears to have changed. |
clay.getHTML() | The exact HTML string a save would send, after all cleanup. |
clay.beforeSave(fn) | Register a callback that receives the cloned document before serialization. The live page is never touched. |
clay.onSnapshot(fn) | Like beforeSave, but runs for every snapshot, including live-sync broadcasts. |
clay.isEditMode | Whether this session may edit (see edit mode). |
clay.isOwner | Whether the platform's owner cookie is set. Unlike isEditMode, the URL and global overrides don't affect it. |
clay.toggleEditMode() | Flip between edit and view mode (reloads the page). |
clay.cacheBust(el) | Re-download one resource by stamping ?v= onto its href or src. Details. |
clay.Mutation | The shared mutation hub. See Advanced. |
clay.morph(oldEl, newEl) | The DOM morphing engine: morphs oldEl in place to match newEl (sync plugin). |
clay.undo | The undo singleton: clay.undo.undo() / clay.undo.redo() (undo plugin). |
clay.cms | The content panel: clay.cms.open() (cms plugin). |
In view mode the edit-only members are absent: window.clay holds just ready, toggleEditMode, isEditMode, isOwner, and Mutation (plus morph/cms when those plugins load). Feature-detect with 'save' in clay.
The HTML surface
| Attribute | Where | What it does |
|---|---|---|
editable | any element | Rich text editing with a floating toolbar (richclay, default on). Tokens: editable="single-line no-toolbar toolbar-on-select". Native contenteditable also works for plain text. |
persist | form controls | Writes the control's current value into the HTML so it survives the save. |
trigger-save | buttons, links | Clicking it calls clay.save(). Same as wiring onclick yourself. |
autosave | <html> | Save automatically after edits settle. Debounced and throttled. |
clay="…" | any element | Region control: no-save, no-snapshot, no-trigger-autosave, no-watch, no-undo, freeze. Full reference. |
merge="name" | JSON script tags | Multi-writer safety for JSON stored in a script tag (sync plugin). Incoming saves three-way merge the JSON per key against the last synced version instead of replacing the blob, so unsaved local keys survive. Arrays merge by identity (id-style fields; name your own with merge-key="taskId"). Bodies may use relaxed JSON: unquoted keys, single quotes, trailing commas, comments. Requires a JSON type. |
editmode:contenteditable | any element | Editable for the owner, inert for visitors. |
editmode:onclick | any element | The onclick runs only for the owner; it's saved inert so it never runs for visitors. |
editmode:resource | style, link, script | Active for the owner, saved with an inert type so visitors never load it. |
viewmode:disabled / viewmode:readonly | form controls | Disabled or read-only for visitors, live for the owner. |
onbeforesave / onbeforesnapshot / onaftersave | any element | Inline hooks into the save. Details. |
savestatus | <html>, set by clayjs | Read-only state for your CSS: saving, saved, error, offline. |
Listening in
| Event (on document) | Fires when | detail |
|---|---|---|
clay:ready | clayjs finished booting (same moment clay.ready resolves) | {clay} |
clay:save-saving | a save has been in flight for 500ms (fast saves skip straight to the result) | {msg, timestamp} |
clay:save-saved | the server confirmed the write | {msg, timestamp} |
clay:save-error | the server answered with a problem | {msg, timestamp} |
clay:save-offline | the browser is offline (clayjs re-saves when the connection returns) | {msg, timestamp} |
clay:sync-applied | a live-sync update landed (sync plugin) | {seq} |
clay:sorted | a drag-drop reorder landed (sortable plugin); fires on the container and bubbles | {item, from, to, oldIndex, newIndex} |
clay:view-save-attempt | a visitor clicked a [trigger-save] element in view mode; show your own notice | — |
Who may edit
Saving requires edit mode. On the platforms, you never think about this: hyperclay.com arms it with a cookie when you're the owner, and Hyperclay Local and htmlclay arm it for local files automatically. Resolution order:
| Priority | Source |
|---|---|
| 1 | ?editmode=true / ?editmode=false in the URL |
| 2 | window.clayEditMode set before clay.js loads |
| 3 | The platform's owner cookie |
Serving files from your own server? Arm it yourself:
<script>window.clayEditMode = true</script>
<script src="https://clayjs.com/clay.js"></script>
Edit mode is client-side behavior. Whether a save is accepted is always the server's decision.
The endpoint spec
A clayjs server implements one route:
| Part | Value |
|---|---|
| Route | POST /_/save |
| Body | The file's full HTML, as plain text. Exception: on localhost, a JSON envelope {content, snapshotHtml, userDriven} with Content-Type: application/json — the dialect Hyperclay Local speaks. Your HTML is content. |
| Header | Page-URL: the URL of the page being saved |
| Success | 200 with JSON { "msg": "Saved" } |
| Failure | Any non-2xx with JSON { "msg": "why" } |
A complete server in about 12 lines of Express:
import express from "express";
import fs from "node:fs/promises";
const app = express();
app.use(express.static("."));
app.use(express.text({ type: "*/*", limit: "10mb" }));
app.post("/_/save", async (req, res) => {
const path = new URL(req.get("Page-URL")).pathname;
let html = req.body;
if (req.is("application/json")) html = JSON.parse(html).content; // localhost envelope
await fs.writeFile("." + path, html);
res.json({ msg: "Saved" });
});
app.listen(4600);
A real deployment adds auth and path validation; this is the whole protocol though. The same contract powers hyperclay.com, Hyperclay Local, and htmlclay. Two details you may ignore: every save also carries an X-Hyperclay-User-Driven: 1|0 header (whether a person triggered it), and htmlclay uses a token variant, POST /_/save/{token}, read from <html htmlclaytoken>.