Docs

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.

Loading

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:

ParamMeaningExample
pluginsComma-separated plugins to loadclay.js?plugins=sync,undo
excludeRemove a default-on pluginclay.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.

Core API

window.clay

MemberWhat it does
clay.readyA 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.isEditModeWhether this session may edit (see edit mode).
clay.isOwnerWhether 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.MutationThe shared mutation hub. See Advanced.
clay.morph(oldEl, newEl)The DOM morphing engine: morphs oldEl in place to match newEl (sync plugin).
clay.undoThe undo singleton: clay.undo.undo() / clay.undo.redo() (undo plugin).
clay.cmsThe 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.

Attributes

The HTML surface

AttributeWhereWhat it does
editableany elementRich 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.
persistform controlsWrites the control's current value into the HTML so it survives the save.
trigger-savebuttons, linksClicking it calls clay.save(). Same as wiring onclick yourself.
autosave<html>Save automatically after edits settle. Debounced and throttled.
clay="…"any elementRegion control: no-save, no-snapshot, no-trigger-autosave, no-watch, no-undo, freeze. Full reference.
merge="name"JSON script tagsMulti-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:contenteditableany elementEditable for the owner, inert for visitors.
editmode:onclickany elementThe onclick runs only for the owner; it's saved inert so it never runs for visitors.
editmode:resourcestyle, link, scriptActive for the owner, saved with an inert type so visitors never load it.
viewmode:disabled / viewmode:readonlyform controlsDisabled or read-only for visitors, live for the owner.
onbeforesave / onbeforesnapshot / onaftersaveany elementInline hooks into the save. Details.
savestatus<html>, set by clayjsRead-only state for your CSS: saving, saved, error, offline.
Events

Listening in

Event (on document)Fires whendetail
clay:readyclayjs finished booting (same moment clay.ready resolves){clay}
clay:save-savinga save has been in flight for 500ms (fast saves skip straight to the result){msg, timestamp}
clay:save-savedthe server confirmed the write{msg, timestamp}
clay:save-errorthe server answered with a problem{msg, timestamp}
clay:save-offlinethe browser is offline (clayjs re-saves when the connection returns){msg, timestamp}
clay:sync-applieda live-sync update landed (sync plugin){seq}
clay:sorteda drag-drop reorder landed (sortable plugin); fires on the container and bubbles{item, from, to, oldIndex, newIndex}
clay:view-save-attempta visitor clicked a [trigger-save] element in view mode; show your own notice
Edit mode

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:

PrioritySource
1?editmode=true / ?editmode=false in the URL
2window.clayEditMode set before clay.js loads
3The 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.

Host it yourself

The endpoint spec

A clayjs server implements one route:

PartValue
RoutePOST /_/save
BodyThe 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.
HeaderPage-URL: the URL of the page being saved
Success200 with JSON { "msg": "Saved" }
FailureAny 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>.