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.addDocumentTransform(fn) | Register a callback that receives the cloned document before serialization. The live page is never touched. |
clay.onSnapshot(fn) | Like addDocumentTransform, 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.
Three tiers, and only the first two are a promise:
| Tier | What it is | Promise |
|---|---|---|
clay.* from clay.js | The everyday surface, above | Stable |
clay.* from a satellite | Opt-in, one script tag each: clay-ui, clay-utils, clay-dom, clay-events, clay-options, clay-internals | Stable |
Anything else under src/ | Reachable by direct import, because src/ ships to npm and the CDN | May change in any release |
The contract starts at 0.3.0: no name in the first two tiers changes without a major version. Everything in the third tier works today and is welcome to be used, but it is not a promise.
clay.internals
The pieces clayjs builds itself out of, for code that needs to sit inside the save lifecycle rather than call it. Lower level than clay.* deliberately: it assumes you know the lifecycle. One extra script tag, and clay.loaded.internals resolves when it's ready.
<script src="https://clayjs.com/clay-internals.js"></script>
| Member | What it does |
|---|---|
captureSnapshot() | The cloned document element, before anything is stripped. |
captureForSave() | The exact bytes a save would send, stripping and all. |
addDocumentTransform(fn) | Register a hook that runs on the clone during save preparation and on every change check, so keep it pure and repeatable. Same registry as clay.addDocumentTransform. |
region.resolveRegionPolicy(node) | The resolved region axes for a node: {watched, autosaveTriggered, undoable, persist, extension}. |
region.isInert(node) | Whether a node is invisible to the whole mutation system (no-watch and extension noise). |
region.isSnapshotRemoved(el) | Whether an element is dropped from every snapshot (no-snapshot). |
region.addRegionToken(el, token) | Add one token to an element's clay attribute, keeping the tokens already there. |
region.PERSIST / region.REGION_ATTRS | The persist values (full, frozen, none) and the canonical token list. |
region.selectors | stripFromSave, stripFromComparison, snapshotRemove, freeze: the selectors clayjs itself strips with, legacy spellings included. |
save.saveHtml(html, cb, opts) | Send an HTML string to the save endpoint. Resolves {ok, msg, msgType, code, etag}; never rejects. |
save.replacePageWith(url, cb) | Fetch another URL's HTML and save it over the current file. Same result shape. |
save.isSaveInProgress() | Whether a save is in flight right now. |
save.saveHtml writes the bytes you hand it straight to the file: it bypasses the snapshot pipeline entirely, so nothing is stripped and no hook runs. Check save.isSaveInProgress() first, because firing into an in-flight save is how you lose an edit. For a worked example of the region helpers, see Advanced.
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>.