# clayjs > clayjs makes an HTML file malleable: the page you are looking at can be edited like a > document and saved, whole, back into its own file. One script tag, no build step, no > account. The document is the app AND the database: state lives in the DOM, and a save > is one POST of the entire serialized document to the host that holds the file. This file is the complete reference for agents, condensed from https://clayjs.com/docs, https://clayjs.com/advanced, and https://clayjs.com/plugins. Human-readable versions live at those URLs. clayjs is MIT-0 licensed. npm builds for bundlers: @panphora/clayjs. Building a whole app (inline editing, controls, sync, undo, derived values)? Read the guide, https://clayjs.com/guide.md: the model, a complete starter file, architecture, recipes, limits and a trap index, with four tested example apps. Every page on clayjs.com also has a Markdown copy at the same path plus `.md`; https://clayjs.com/docs.md lists the reference pages. ## The one-paragraph integration To make a static HTML file malleable: add `` as the last element inside ``. Add an `autosave` attribute to the `` element. Add an `editable` attribute to each element a person should edit as rich text (headings, paragraphs, note containers). Add a `persist` attribute to any form control whose value should survive saving. Mark editing controls with `show-when:editmode="true"` and load `clay-options.js`, so viewers never see them. Put UI that a script builds or changes at runtime inside `clay="editor-ui"`. Do not write any save logic or add a save button: clayjs saves the whole document automatically, and the host writes it to disk. ## Hosts (who writes the file) clayjs is client-side; a host accepts the save and writes bytes: - HTML Clay, a small desktop app (https://htmlclay.com): serves local `.htmlclay` files at http://127.0.0.1, writes saves back to disk, keeps automatic version history. - hyperclay.com: hosts the same files online. - Your own server: implement one route (see "The endpoint spec" below, ~20 lines). ## Loading ```html ``` The default build includes the save lifecycle and rich text (`editable`). Plugins load conditionally through the URL, in the browser only: - `?plugins=` comma-separated plugins to add, e.g. `clay.js?plugins=sync,undo` - `?exclude=` remove a default-on plugin, e.g. `clay.js?exclude=richclay`. One `exclude=` takes a comma list (`?exclude=source,richclay`); a second `exclude=` parameter is ignored. Loadable plugins: `richclay` (default on), `source` (default on), `ai-edit` (brings `wire`), `indicator`, `sync`, `live`, `sortable`, `undo`, `cms`, `quickcrop`, `upload`, `people`, `wire`, `demo`, `visitor-drafts`. The default-on plugins load in edit mode only. `visitor-drafts` turns the page into a free document: a visitor who is not the owner gets the editing UI and their work is kept in their own browser (guide section 14). Everything else is a separate library ("satellite") with its own script tag: clay-ui, clay-events, clay-options, clay-dom, all.js, clay-utils, clay-internals, clay-data, sap.js. To call a satellite from an inline script right after its tag, wait for it: `await clay.loaded.ui` (or `.dom`, `.all`, `.utils`, `.events`, `.options`, `.data`, `.sap`, `.internals`). Offline: `https://clayjs.com/v1/clay.standalone.js` is the whole library in one readable file (core, every plugin, every satellite), built from the same source. Download it, keep it beside the HTML file, and load it with the same query params: ``. `plugins=` then decides what runs, not what downloads, and every `clay.loaded.*` promise exists without a satellite tag. Delete any satellite tags the page had: the file carries them all, and a second `sap.js` mounts a second runtime. `clay.Sap` and `clay.extractData`, which a tag defines synchronously, arrive one microtask later here; `await clay.loaded.sap` (or `.data`) covers both. `/1.1.0/clay.standalone.js` is that exact release, pinned, and every later release has the same form; the npm tarball carries it at `dist/clay.standalone.js`; the file's first line names its version. Two things still reach the network, both opt-in: the CMS sidebar's font comes from jsDelivr and falls back to the system monospace, and autosave debugging (`localStorage` key `clay:debug:autosave`) imports its diff library from esm.sh. Saving still needs a host that writes the file; HTML Clay does that on the machine itself, with no network. ## Core API: window.clay - `clay.ready` — 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.theme(options)` — theme the UI ClayJS draws (dialogs and the people name prompt). `options` is `{ auto, colorScheme, tokens, parts }`, the same shape as `window.clayTheme` set before the loader. `clay.theme(false)` keeps the exact Bevel look, `clay.theme(null)` restores the default (the page's font and solid colours over Bevel), `clay.theme()` returns the current configuration. Invalid options throw a `TypeError` and keep the previous theme. Applies to the next surface drawn. See `docs/injected-ui.md`, "Page themes". - `clay.save()` — snapshot the page and save it. Skips when nothing changed. Returns `Promise<{ok, msg, msgType, code, etag}>`; check `ok`. `msgType` is `success`, `error`, `skipped` (nothing was sent), `conflict` (HTTP 412, see `clay:save-conflict`; the result also carries `changedBy`, `afterTimeout` and `conflictEtag`), or `unknown` (the request timed out, so the write may or may not have landed). A host may answer with its own severity, such as `warning`. `skipped` covers both "No changes to save" and "Save already in progress" (the bytes are queued and sent after the in-flight save), so branch on `msgType`, not `ok`. Never rejects. - `clay.save.force()` — save even when nothing appears to have changed. - `clay.save.flush({keepalive, timeoutMs})` — save now, and answer only when the host has accepted the bytes the page holds at that moment. Resolves `{state, etag}`, where `state` is `view` (this page cannot save), `clean` (the file already holds these bytes, so nothing was sent) or `saved` (this call wrote them, and `etag` is the stamp the host accepted). Rejects with an `Error` whose `state` is `failed`, `conflict` or `blocked`: `blocked` means the bytes are on the host but the page holds work a save cannot write, reported by a check registered through `clay.registerUnsavedState`, so only a person can resolve it. A save already on the wire is waited out; an edit made while a request is in flight forces another save. A refusal is reported, never resolved by overwriting: keeping this tab's version stays a person's decision (`clay.save.overwrite`). In the first seconds after load it waits for the page to settle, so load-time rendering is not written as an edit. `keepalive` asks the transport to see the request through a page unload and is honoured under a 60 KB UTF-8 body only; `timeoutMs` (default 15000) bounds the whole call. - `clay.hasUnsavedChanges()` — synchronous answer to "is there work here that no save has written?", the same question the close warning asks: an edit in the DOM, or work a check registered through `clay.registerUnsavedState` reports as pending. `false` in view mode. - `clay.extractData(rules?)` — read structured JSON from the live page. Supplied by the `clay-data.js` satellite. Omitted rules use the page's `api` rules tag. - `clay.applyData(values, {rules})` — apply structured JSON to the live page atomically. Supplied by the `clay-data.js` satellite. It changes content only and refuses executable content, unsafe attributes, unknown keys, and HTML replacement without changing the page. - `clay.readData(rules?)` — read the saved file as `{data, etag}`. Always present in core, and rejects when the host does not announce `data-read`, and in draft mode, where the saved file is not this visitor's to read, with `code: "DataUnavailableInDraft"`. - `clay.writeData(values, {rules, ifMatch})` — conditionally write JSON into the saved file and return `{data, etag}`. Always present in core, and rejects when the host does not announce `data-write`, and in draft mode with `code: "DataUnavailableInDraft"`. - `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. Runs on every change check too, so keep it pure and repeatable (no timestamps, counters, or network). - `clay.onSnapshot(fn)` — like `addDocumentTransform`, but runs for every snapshot, including live-sync broadcasts. - `clay.markDirty()` — tell live sync the person just edited something it cannot see: an editor whose own UI is `editor-ui` and which writes its data itself. Call it on each user edit. Without it, an edit made while an incoming update waits on a script or stylesheet load can be recorded as already saved. Present on every edit-mode page, and in view mode when `sync` is loaded. - `clay.isEditMode` — whether this session may edit. - `clay.isOwner` — whether the platform's owner cookie is set (URL and global overrides don't affect it, unlike `isEditMode`). - `clay.isDraftMode`: whether this visitor's work is being kept in their own browser instead of saved to the host. Needs a page that opted in with `visitor-drafts` (a slot on ``, the plugin in `plugins=`, an http(s) page, an `api` rules tag), so it is `false` everywhere else. Draft mode also sets `draftmode="true"` on ``, removed from every save (visitor-drafts plugin). - `clay.visitorDrafts`: the draft store, with `active`, `slot`, `key`, `hasDraft()`, `restored()` (`{restored, hadDraft, parked}`), `saveNow()`, `reset()`, `onRestore(fn)`, `editOriginal()`, `backToDraft()`, `downloadUrl()`, `source`. Inert outside draft mode. Events: `clay:drafts-restored`, `clay:drafts-saved`, `clay:drafts-store-failed` (visitor-drafts plugin). - `clay.siteVisibility` — `"private"` or `"public"` when the host says how it serves this site (hyperclay.com tells the owner's tab), otherwise `null`. Read once, at boot. - `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`. - `clay.Mutation` — the shared mutation hub (one MutationObserver for the whole page, region rules applied). `clay.Mutation.onAnyChange(opts, cb)` respects `clay="no-watch"` regions. - `clay.morph(oldEl, newEl)` — content-based DOM morphing engine: morphs `oldEl` in place to match `newEl`, preserving focus, inputs, and animations (sync plugin). Its signature is unchanged on hyper-morph 1.0. - `clay.undo` — document-wide undo singleton: `clay.undo.undo()` / `clay.undo.redo()` (undo plugin). - `clay.ephemeral` — per-tab state on saved elements: `add(el, word)`, `remove`, `toggle(el, word, force)`, `has`. See Ephemeral state. - `clay.cms` — the content panel: `clay.cms.open()` (cms plugin). - `clay.RichClay` — the rich-text editor class behind `editable` (richclay plugin). - `clay.quickcrop(file, opts)` — crop modal: resolves `{blob, dataURL, width, height}`, or `null` if cancelled (quickcrop plugin). - `clay.upload(file, {onProgress, signal})` — post a `File` you supply (from an ``, a drop or a paste) to the host. Resolves `{ok, msg, msgType, code, uploads}` with `uploads[0].url` on success and never rejects. It opens no picker and inserts nothing: write the URL into the page yourself. `code` is `unsupported` when the host stores no files; other failures include `too-large`, `unsupported-type`, `payment-required`, `unauthorized`, `forbidden`, `timeout`, `aborted`, `network`. Events: `clay:upload-start`, `clay:upload-progress`, `clay:upload-done`, `clay:upload-error` (upload plugin). - `clay.me` — the person editing: `{id, name, initials, color}` or `null` (people plugin). The host names you through `/_/meta`: hyperclay.com from your account, HTML Clay and Hyperclay Local from their Profile setting when you turn it on (Local uses your Hyperclay account while signed in). Otherwise it is the name this browser gave the first time you authored something. `color` is an index from 0 to 7; the eight colors in order are `#6b4fa8`, `#2f6a9e`, `#9a6a1c`, `#a8463d`, `#2f7a69`, `#4f6178`, `#3f7d3a`, `#a2457a`, and the presence stack paints index *n* with the *n*th, so an app that uses them shows a person in the same color as the stack. Attribution, never a permission check. - `clay.people` — `get(id)` (always a record, "Unknown person" for an id nobody named), `list()` (the people this document names), `await available()` (the host's team for a picker, refreshed, or `null`), `add(person)` (record a person in the document's `