When you need more
Everything here is optional. The first half is advanced control over the save lifecycle itself; the second half is separate libraries you add with their own script tags.
Region reference
One attribute controls how any element takes part in saving. Keywords combine with spaces: clay="freeze no-undo".
| Keyword | Plain meaning | Typical use |
|---|---|---|
no-save | Lives on the page, never written to disk. Live sync leaves your copy alone. | Search boxes, connection badges, edit toolbars |
no-snapshot | Invisible to save and to sync. Every device keeps its own copy. | Per-device panel state |
freeze | Saved exactly as authored; runtime changes to its contents are ignored. | Clocks, tickers, computed summaries |
no-trigger-autosave | Saved normally, but edits here don't trigger an autosave. | High-churn text you'd rather batch |
no-watch | The mutation system ignores it entirely (implies no autosave trigger, no undo). | Third-party widgets that churn the DOM |
no-undo | Changes here aren't recorded in undo history. | Live regions where undo would feel wrong |
Live or computed text (clocks) → clay="freeze"
Third-party embeds → clay="no-watch"
Heavy editors, batch the saves → clay="no-trigger-autosave"
Respecting regions from your own code
Writing a custom attribute that touches the save? Ask clayjs what a region means instead of matching [no-save] yourself. That's what clay.internals.region is for: it's the same policy the library uses, so your attribute can't quietly stop agreeing with it two releases from now. It needs one extra script tag — the internals satellite.
<script src="https://clayjs.com/clay-internals.js"></script>
A count-of attribute that writes how many elements match a selector into a badge, and leaves alone the regions that aren't being persisted as they stand:
await clay.loaded.internals;
const { resolveRegionPolicy, addRegionToken, PERSIST } = clay.internals.region;
clay.internals.addDocumentTransform(clone => {
for (const el of clone.querySelectorAll("[count-of]")) {
// Inside no-save or freeze? This element isn't going to disk as it stands,
// so counting into it is either wasted or wrong.
if (resolveRegionPolicy(el).persist !== PERSIST.FULL) continue;
el.textContent = clone.querySelectorAll(el.getAttribute("count-of")).length;
}
});
Keep the callback pure and repeatable. It runs on the save clone, and it runs again every time clayjs checks whether the page has changed. Write something new on each run, a timestamp being the tempting one, and every check differs from the last, so the page never reads clean and autosave never stops. Derive from the document, don't stamp the clock.
When your attribute generates chrome of its own, mark it rather than hoping nobody saves it. addRegionToken merges into whatever clay attribute is already on the element:
addRegionToken(toolbar, "no-save"); // clay="existing-token no-save"
The rest of the surface — isInert, isSnapshotRemoved, REGION_ATTRS, and the raw selectors clayjs strips with — is in the reference.
Advanced save attributes
“I want to clean an element up right before it's written to disk.”
Inline handlers that run on a copy of the element during the snapshot, so the live page never flickers. onbeforesave runs only for saves; onbeforesnapshot runs for every snapshot, including live-sync broadcasts.
<div onbeforesave="this.removeAttribute('data-temp')">…</div>
“Run something every time a save succeeds.”
<div onaftersave="console.log('saved at', event.detail.timestamp)">…</div>
“My stylesheet is generated on save; reload it without a flash.”
<link rel="stylesheet" href="style.css" refetch-on-save>
“Force one resource to re-download.”
Swaps a ?v= timestamp onto the element's href or src. Pairs well with onaftersave.
“I need JavaScript-level control over the outgoing HTML.”
Registers a callback that receives the cloned document before serialization, and again on every change check. Strip, rewrite, or annotate anything; the live page is untouched. Keep it pure and repeatable: a callback that writes something new each run leaves the page permanently dirty.
clay.addDocumentTransform(doc => doc.querySelectorAll(".draft-marker").forEach(el => el.remove()));
Bring them in when the problem shows up
These aren't checkboxes; each is its own library with its own script tag and docs. They share clayjs's taste: HTML-first, no build step.
Each loads as a plain <script>. To call one from an inline script right after its tag, wait for it first: await clay.loaded.ui (or .dom, .all, .utils, .events, .options, .data, .sap).
“I need a toast or a confirm dialog and don't want to build one.”
Ready-made toasts, modals, and ask/confirm/tell dialogs. It listens to clay:save-* events automatically, so adding it upgrades your save feedback for free. If you already have a toast library, keep yours: the events are public.
<script src="https://clayjs.com/clay-ui.js"></script>
“I want small behaviors without writing addEventListener boilerplate.”
onclickaway, onclickchildren, onrender, onmutation, onclone: HTML attributes for the events HTML forgot.
<script src="https://clayjs.com/clay-events.js"></script>
“Show or hide parts of the page based on a state attribute.”
Declarative show/hide: an ancestor carries the plain state attribute view="kanban", descendants opt in or out with option:view / option-not:view attributes.
<script src="https://clayjs.com/clay-options.js"></script>
“closest() only searches up. I need the element near me.”
el.nearest.menu, el.val.title, el.exec.action(), el.cycle(1, "state"): small prototype helpers for DOM-first apps. The first three are getter proxies (read the nearest element with that attribute or class); cycle takes its attribute name.
<script src="https://clayjs.com/clay-dom.js"></script>
“I miss jQuery's ergonomics, not its size.”
A chainable wrapper over querySelectorAll: All(".card").css({ opacity: 1 }).onclick(e => e.target.classList.toggle("done")). Events are property-form (.onclick, .oninput); classes go through .classList. Zero dependencies, one script tag.
<script src="https://clayjs.com/all.js"></script>
“The small stuff: throttle, debounce, cookies, slugify.”
<script src="https://clayjs.com/clay-utils.js"></script>
“I want the morphing engine itself.”
The content-based DOM morphing that powers live sync ships inside the sync plugin and is exported as clay.morph. Use it to update a page in place while preserving focus, inputs, and animations. JSON script tags marked merge="name" are three-way merged per key during sync instead of overwritten.
“One MutationObserver for the whole page, with region rules applied.”
The shared mutation hub that autosave and live sync sit on. clay.Mutation.onAnyChange(opts, cb) respects clay="no-watch" regions so your observers and clayjs's agree about what counts as a change.