clay.js
  1. ClayJS
  2. How ClayJS works
  3. 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:

HTML
<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 reads document.currentScript to find its own URL and locate its modules. As type="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.js from /1.8.2/ with clay-ui.js from /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 for DOMContentLoaded anyway.

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.

PluginDefaultLoads in view modeAdds
richclayonnothe editable attribute, clay.RichClay
sourceonnobyte-preserving saves, clay.source
syncoffyeslive sync, clay.conflicts, clay.morph, presence and the editing marker
liveoffyesclay.live(name): a clay="live" JSON tag as a live object
sortableoffnothe sortable attribute, window.Sortable, clay:sorted
undooffnoclay.undo (whole-page undo, takes Cmd+Z for the window)
indicatoroffnothe save status chip
uploadoffnoclay.upload
quickcropoffyesclay.quickcrop
cmsoffyesclay.cms; implies quickcrop and upload
wireoffyesclay.wire, the channel to a local agent process
messageoffyesmessage-form: a visitor’s contact form to the owner, clay.message (message: contact forms)
ai-editoffnoselect text, ask an agent to rewrite it; implies wire
demooffyesfakes saving into localStorage for public demos
visitor-draftsoffyesclay.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)
deriveon for any page with a [sap] rootyesthe 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 (or clay.modeLifecycle.canWrite() before a write), never 'save' in clay or clay.RichClay alone, 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).
  • exclude wins over implications. exclude=wire also drops ai-edit.
  • Load order is fixed regardless of how you list them.
  • Use exclude=richclay when the app has no rich text. exclude=source saves 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, page loads only its reader half and nothing it implies. A Document on plugins=page resolves to page-view, with no live data service, no generic sync, and no upload. Name sync as well (plugins=page,sync) when the reader should take its saved files from sync’s lane instead of its own.
  • derive loads automatically when the page has a [sap] root at boot, and stays idle when full sapjs is on the page. exclude=derive opts out; plugins=derive loads it for a page that adds its [sap] root later.
  • visitor-drafts loads its draft store only in draft mode. A reader or an owner editing the original gets a small module that publishes an inert clay.visitorDrafts and, for the owner, the owner chip. On a page with no visitor-drafts slot, no api rules tag, or a rules tag this version cannot read (not data-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.

TagPromiseAddsSection
clay-ui.jsclay.loaded.uiclay.toast, toastPersistent, ask, confirm, tell, snippet, modal; automatic save feedback toastsclay-ui: toasts and dialogs
clay-options.jsclay.loaded.optionsshow-when:, hide-when:, option:, option-not: visibilityShowing and hiding edit controls: clay-options
clay-events.jsclay.loaded.eventsonclickaway, onclickchildren, onclone, onmutation, onglobalmutation, onrenderclay-events, clay-dom and All
clay-dom.jsclay.loaded.domel.nearest, el.val, el.text, el.exec, cycleAttr, clay.dom.createContentViewclay-events, clay-dom and All
all.jsclay.loaded.allAll(selector) collection helperclay-events, clay-dom and All
clay-utils.jsclay.loaded.utilsclay.utils.debounce, throttle, cookie.get/remove, slugify, copyToClipboard
clay-internals.jsclay.loaded.internalsclay.internals: capture, transforms, raw save
clay-data.jsclay.loaded.dataclay.extractData, clay.applyDataImport and export
sap.jsclay.loaded.sapSap, the reactive attribute layersap.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:

JavaScript
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 } }:

MarkExact boundary
document:bootan installation starts, before the source is validated
document:assets-readythe editor’s code is in hand
document:authority-readythe capability and first-save gates passed, the page handle is admitted and the binding is created
document:input-readyadoption 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:

HTML
<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, and var(--your-variable, fallback) works. A page can also set a token in CSS as --clay-ui-<name> (--clay-ui-accent-text for accentText). spacing is one length, such as 20px.
  • Parts set anything else. Every element ClayJS draws has a stable name in data-clay-part, such as dialog.panel, dialog.title, button.primary, or people.choice. Each takes base, hover, active, focus, and disabled declarations. A part names its surface, so people.title overrides dialog.title in the name prompt only. The full list is in docs/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 = false keeps the exact built-in look. After loading, clay.theme(options) replaces the theme, clay.theme(false) and clay.theme(null) restore the built-in look or the default, and clay.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:

JavaScript
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.

JavaScript
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.

Part of the ClayJS complete guideDownload the whole guide as Markdown