- ClayJS
- How ClayJS works
- Add ClayJS to a page
Add ClayJS to a page
The loader tag, versions, plugins and satellites, and how ClayJS boots.
The loader tag#
Load ClayJS with a single <script> tag and select plugins in its query string:
<script src="https://clayjs.com/v1/clay.js?plugins=sync,sortable,undo,indicator&exclude=richclay"></script>
- Use a classic script with a
src. The loader readsdocument.currentScriptto find its own URL and locate its modules. Astype="module"or pasted inline, it logs an error and never boots. /v1/is the rolling 1.x alias, cached for 10 minutes: files get fixes automatically./1.8.2/and every exact version are immutable for a year: use one when a file must never change behavior.- Use the same URL prefix on every tag. Companion scripts (satellites, such as
clay-ui.js) import ClayJS modules relative to their own URL.clay.jsfrom/1.8.2/withclay-ui.jsfrom/v1/gives two copies of shared modules: transforms registered through one are invisible to the other, and save feedback appears twice. - npm and jsDelivr package paths work too (
@panphora/clayjs). - Put the tags at the end of
<body>. The loader waits forDOMContentLoadedanyway.
plugins= and exclude= are comma-separated lists. Only the first of each parameter counts. Unknown names warn and are ignored.
Plugins#
Plugins add editing, sync, and UI features to clay.js. Most are off by default, and edit-only plugins skip view mode.
| Plugin | Default | Loads in view mode | Adds |
|---|---|---|---|
richclay | on | no | the editable attribute, clay.RichClay |
source | on | no | byte-preserving saves, clay.source |
sync | off | yes | live sync, clay.conflicts, clay.morph, presence and the editing marker |
live | off | yes | clay.live(name): a clay="live" JSON tag as a live object |
sortable | off | no | the sortable attribute, window.Sortable, clay:sorted |
undo | off | no | clay.undo (whole-page undo, takes Cmd+Z for the window) |
indicator | off | no | the save status chip |
upload | off | no | clay.upload |
quickcrop | off | yes | clay.quickcrop |
cms | off | yes | clay.cms; implies quickcrop and upload |
wire | off | yes | clay.wire, the channel to a local agent process |
message | off | yes | message-form: a visitor’s contact form to the owner, clay.message (message: contact forms) |
ai-edit | off | no | select text, ask an agent to rewrite it; implies wire |
demo | off | yes | fakes saving into localStorage for public demos |
visitor-drafts | off | yes | clay.visitorDrafts; in draft mode, the draft store, download prompt and intro area; outside it, only the owner chip, fetched when the owner edits (Free documents for visitors) |
derive | on for any page with a [sap] root | yes | the sapjs core: records and paints, treated as derived (sap.js: records, views and totals in markup) |
- Edit-only plugins do not load in a tab that booted in view, even when named. Their members are absent there. Test
clay.isEditMode(orclay.modeLifecycle.canWrite()before a write), never'save' in clayorclay.RichClayalone, because a live exit leaves those members attached. On a<html clay-mode-switch="live">document, entering edit imports them while the tab still shows view, and they stay loaded after the exit (Leaving edit). excludewins over implications.exclude=wirealso dropsai-edit.- Load order is fixed regardless of how you list them.
- Use
exclude=richclaywhen the app has no rich text.exclude=sourcesaves one request and about 48 KB, at the cost of saves reprinting the whole document instead of keeping hand formatting. - Nothing implies
undo, on purpose: it intercepts Cmd+Z in every text field on the page (undo: whole-page undo). - In view mode,
pageloads only its reader half and nothing it implies. A Document onplugins=pageresolves topage-view, with no live data service, no genericsync, and noupload. Namesyncas well (plugins=page,sync) when the reader should take its saved files from sync’s lane instead of its own. deriveloads automatically when the page has a[sap]root at boot, and stays idle when full sapjs is on the page.exclude=deriveopts out;plugins=deriveloads it for a page that adds its[sap]root later.visitor-draftsloads its draft store only in draft mode. A reader or an owner editing the original gets a small module that publishes an inertclay.visitorDraftsand, for the owner, the owner chip. On a page with novisitor-draftsslot, noapirules tag, or a rules tag this version cannot read (notdata-rules-version="1", or not JSON), it warns in the console and draws nothing. Free documents for visitors covers the opt in.
Measure what each mode you ship downloads, and keep a byte budget and a test that fails when one grows: Keep a byte budget per boot, and fail a test when it grows.
A good default for apps: ?plugins=sync,sortable,undo,indicator, plus upload for images or attachments, plus exclude=richclay when there is no rich text.
Satellites#
Satellites are separate classic script tags. Each publishes a readiness promise on clay.loaded.
| Tag | Promise | Adds | Section |
|---|---|---|---|
clay-ui.js | clay.loaded.ui | clay.toast, toastPersistent, ask, confirm, tell, snippet, modal; automatic save feedback toasts | clay-ui: toasts and dialogs |
clay-options.js | clay.loaded.options | show-when:, hide-when:, option:, option-not: visibility | Showing and hiding edit controls: clay-options |
clay-events.js | clay.loaded.events | onclickaway, onclickchildren, onclone, onmutation, onglobalmutation, onrender | clay-events, clay-dom and All |
clay-dom.js | clay.loaded.dom | el.nearest, el.val, el.text, el.exec, cycleAttr, clay.dom.createContentView | clay-events, clay-dom and All |
all.js | clay.loaded.all | All(selector) collection helper | clay-events, clay-dom and All |
clay-utils.js | clay.loaded.utils | clay.utils.debounce, throttle, cookie.get/remove, slugify, copyToClipboard | |
clay-internals.js | clay.loaded.internals | clay.internals: capture, transforms, raw save | |
clay-data.js | clay.loaded.data | clay.extractData, clay.applyData | Import and export |
sap.js | clay.loaded.sap | Sap, the reactive attribute layer | sap.js: records, views and totals in markup |
Satellites do not wait for clay.ready and work without clay.js, including in view mode and on a static host. In code that runs early, await the promise before using what it adds (await clay.loaded.ui before clay.toast). Inside a clay.ready callback, satellites have usually finished loading, so clay.toast?.(...) is safe and the optional call costs nothing.
clay.standalone.js bundles the loader, every plugin, and every satellite into one 2 MB file for offline use. plugins= still decides what runs. With it, remove the satellite tags and always await clay.loaded.sap or .data before use.
In Hyperclay Local and HTML Clay, a document needs no special loading to work offline. Keep the ordinary https://clayjs.com/v1/ tags and the host serves them from its bundled release. Use clay.standalone.js for a file opened with no host at all. Offline in a local host covers the route and what a host must do.
clay.ready#
clay.ready is a promise that resolves with window.clay once boot is done and every plugin has loaded. clay:ready fires on document at the same moment. Before it resolves, members like clay.save and clay.undo do not exist. Start app code with clay.start(...): it waits for clay.ready and runs once per page (Starting the app once).
clay.plugins is the frozen list of plugin names in the plan the loader resolved, late plugins included. It says what the page asked for, not what has finished loading.
clay.ready means the loader is done, not that a plugin has finished its own startup. The page plugin keeps going: it exports no ready, so clay.ready fires while the person still sees the article’s saved content. clay.page.ready is that plugin’s boot, and clay.page.status is its current result, { phase, reason, installId, at }. Await it before touching clay.page:
clay.start("document-ui", async () => {
const boot = await clay.page?.ready;
if (boot?.phase !== "ready") return;
document.documentElement.setAttribute("document-ready", "true");
});
Boot moves through loading (the saved article is read and validated), joining (the editor’s code downloads beside the room join, plus capability, the first save, and the page handle), and adopting (the editor is mounted and the saved article is adopted into the room). It then settles at ready (input enabled), blocked, or error. Each boundary is a performance.mark carrying { detail: { installId } }:
| Mark | Exact boundary |
|---|---|
document:boot | an installation starts, before the source is validated |
document:assets-ready | the editor’s code is in hand |
document:authority-ready | the capability and first-save gates passed, the page handle is admitted and the binding is created |
document:input-ready | adoption succeeded and editable was enabled on the current mounted view |
A failed or cancelled installation never emits document:input-ready. Blocked readiness still resolves: clay.page.ready settles, clay.page.status.phase is blocked with a reason, the saved article stays as the host served it, and no editable page appears. When the page editor is ready covers the reasons and what a page can offer instead.
You can set configuration before the tag, and the loader merges into it: window.clay = { saveToast: true }.
Theming ClayJS’s own UI#
ClayJS draws dialogs (clay.ask, clay.confirm, clay.tell, clay.snippet, clay.modal) and the people plugin’s name prompt. By default they take your page’s font. On a page with a plain solid background, they also take its background and text colours. Everything else comes from ClayJS’s built-in look.
To match them to your design, set window.clayTheme before the loader tag:
<script>
window.clayTheme = {
tokens: { font: "Inter, sans-serif", text: "#263d4e", surface: "#ffffff", border: "#e0e7ee", accent: "#0c2635", accentText: "#ffffff", radius: "8px" },
parts: { "dialog.title": { base: { "font-size": "18px", "font-weight": "600" } } }
};
</script>
<script src="https://clayjs.com/v1/clay.js?plugins=people"></script>
- Tokens set the basics:
font,headingFont,text,mutedText,background,surface,border,accent,accentText,danger,dangerText,success,warning,radius,shadow,overlay,spacing,buttonHeight. Each is a CSS value, andvar(--your-variable, fallback)works. A page can also set a token in CSS as--clay-ui-<name>(--clay-ui-accent-textforaccentText).spacingis one length, such as20px. - Parts set anything else. Every element ClayJS draws has a stable name in
data-clay-part, such asdialog.panel,dialog.title,button.primary, orpeople.choice. Each takesbase,hover,active,focus, anddisableddeclarations. A part names its surface, sopeople.titleoverridesdialog.titlein the name prompt only. The full list is indocs/injected-ui.md. - Values win in this order:
tokens, then--clay-ui-*properties, then the page’s sampled font and colours, then the built-in look. window.clayTheme = falsekeeps the exact built-in look. After loading,clay.theme(options)replaces the theme,clay.theme(false)andclay.theme(null)restore the built-in look or the default, andclay.theme()returns the current one. A change affects the next dialog, not one already open.
Your stylesheet cannot reach these elements: every declaration is inline and !important, so a stray button { ... } rule never breaks a prompt. On a phone, dialog buttons stay at least 44px tall regardless of the theme.
Plugins that load late#
source, sortable, and menu load after clay:ready, one at a time, in the order the plan resolves them. Nothing on first paint or the first keystroke waits for them. A late plugin that fails to load is logged and left absent, like any other.
Each late plugin fires clay:plugin-ready on document once its own ready settles, with detail.plugin naming it. Use this event to tune something the plugin built for itself. For example, a Kanban board waits for sortable and then sets the drag delay its cards need on a phone:
document.addEventListener("clay:plugin-ready", (event) => {
if (event.detail.plugin !== "sortable") return;
for (const list of document.querySelectorAll("ol.cards")) {
const sortable = window.Sortable?.get?.(list);
sortable?.option("delay", 220);
sortable?.option("delayOnTouchOnly", true);
}
});
Do not read window.Sortable in a clay.ready callback: the plugin may not have arrived. clay.plugins tells you it is coming; clay:plugin-ready tells you it is here.
Libraries on demand#
clay.assets.load(name) loads a library from the same release that served clay.js the first time a page asks for it. The document needs no vendor URL, so a copy served by another host or opened from disk still finds it. The names are overtype, marked, and highlight, plus one grammar per highlight/<language> for bash, css, javascript, json, python, and xml. clay.assets.names() lists them.
const { Marked } = await clay.assets.load("marked");
const { default: hljs } = await clay.assets.load("highlight");
const { default: javascript } = await clay.assets.load("highlight/javascript");
hljs.registerLanguage("javascript", javascript);
overtype resolves the vendored OverType editor with its toolbar buttons. marked resolves the Marked class. highlight resolves the core with no grammar of its own, so a page registers only the grammars it uses. Each name loads once: a second call before the first settles shares it, a failed one is forgotten and can be tried again, and an unknown name rejects with clay.assets: no asset named "<name>". The window.clay reference lists what each name resolves to.