clayjs

Malleable HTML files

The tools you need to make a static HTML file behave like a Google Document: editable, auto-save, live sync, etc.

notes.html simulated save

My notes

Click the heading and type. Select text to format it. Then save.

<!DOCTYPE html>
<html>
  <body>
    <h1 editable>My notes</h1>
    <button onclick="clay.save()">Save</button>
    <script src="https://clayjs.com/clay.js"></script>
  </body>
</html>

That's a complete app. Type into the heading, hit Save, refresh: your words are still there. The file saved itself.

Start the tutorial Get your script tag

A malleable HTML file is an HTML file that can save itself. There is no database and no build step: the page you're looking at is the data. When you save, clayjs snapshots the page's HTML, cleans it up, and POSTs it back to the server it came from, which writes it to disk. Next load, the file opens exactly the way you left it.

“I love simple solutions to problems that modern webdev has made unnecessarily complicated.”

kami · developer

Step 1 · Make it save

You changed the page, and a refresh throws it away.

Add the script tag, then call clay.save() whenever you want to persist the page. Wire it to a button, a keystroke, or your own logic:

<script src="https://clayjs.com/clay.js"></script>
<button onclick="clay.save()">Save</button>

<script type="module">
  // or from your own code, with the server's answer:
  await clay.ready;
  const { msg, msgType } = await clay.save();
</script>

What actually happens

clayjs POSTs the file's own HTML to /_/save on the server it was loaded from, and gets back a small JSON answer: { "msg": "Saved" }. That's the whole protocol. Anything that implements that one endpoint can host malleable files:

ServerWhat it is
hyperclay.comHosted platform: accounts, versions, sharing
Hyperclay LocalDesktop app that serves a folder on your machine
htmlclayDesktop app: double-click a .htmlclay file and it just saves
Your ownAbout 12 lines of Express. Endpoint spec.
Step 2 · Know it saved

You hit save. Did it work?

clayjs never draws its own interface. Instead it keeps a savestatus attribute on <html> up to date (saving, saved, error, offline), and you decide what that looks like. Paste this CSS and you have a status chip, no JavaScript needed:

html[savestatus] body::after {
  position: fixed; right: 16px; bottom: 16px;
  padding: 4px 12px; border-radius: 99px; font: 13px system-ui;
}
html[savestatus="saving"] body::after { content: "Saving…"; background: #eee; }
html[savestatus="saved"]  body::after { content: "Saved"; background: #dcefe0; color: #2f5d3a; }
html[savestatus="error"]  body::after { content: "Couldn't save"; background: #f6e3de; color: #a04f38; }
notes.html simulated

Try both outcomes:

The chip is yours: restyle it, move it, delete it.

Don't want to touch CSS? Check “a built-in save indicator” in the configurator and clayjs ships a small ready-made one.

Roll your ownevents + promise

Every save fires events on document and returns a promise, so any UI you already have can listen in:

document.addEventListener("clay:save-saved", () => myToast("Saved"));
document.addEventListener("clay:save-error", e => myToast(e.detail.msg));

// or handle it inline:
const { msg, msgType } = await clay.save();

Events: clay:save-saving, clay:save-saved, clay:save-error, clay:save-offline. Each carries detail.msg.

When it failserror + offline

savestatus="error" means the server answered with a problem; the message is in the event's detail.msg. savestatus="offline" means the request couldn't get out at all. And if someone closes the tab with unsaved changes, clayjs warns them first. That safety net is built in.

Step 3 · Make it a document

A page you can't type into isn't a document.

Put editable on anything: it becomes rich text you can type into, with bold, italics, lists, and links. Form controls need one attribute, persist, which writes their current value into the HTML itself so it survives the save:

<p editable>Meeting notes for Tuesday…</p>
<input name="title" persist>
<textarea name="draft" persist></textarea>
meeting.html simulated save

Meeting notes for Tuesday…

Edit both, save, then reload: everything survives. Reload without saving and your changes are gone, which is the point.

The problems you'll hit next

Saved files have a lifecycle

Three things come up in almost every malleable file, usually in this order. Open the one you're facing.

I shouldn't have to press Save.autosave

Put autosave on the <html> element and clayjs saves for you: edits settle for a moment, then a save fires. It's debounced and throttled, so rapid typing never spams the server.

<html autosave>
journal.html simulated

Type here and just stop. It saves on its own.

It's open in two places and the copies fight.live sync

Check “sync edits across open copies” in the configurator. Edits then flow into every open copy of the file, morphing the page in place: no reloads, and nobody loses their cursor. It's off by default on purpose: it holds a connection open, and you don't need that until you have this problem.

plan.html · your laptop

Type in either copy…

plan.html · your phone simulated

Type in either copy…

Visitors can see my edit controls.edit mode

Your first malleable file can keep its controls exposed; that's fine. When you share it, mark the owner-only parts and clayjs strips or disables them for everyone else. The server decides who the owner is.

<h1 editmode:contenteditable>Only the owner can edit this</h1>
<button clay="no-save" onclick="clay.save()">Save</button>
recipe.html simulated

Sourdough, third attempt

More water, less patience.

Visitors won't see these buttons or the dashed underline.

One more concept

Control what gets saved

Some parts of the page shouldn't be part of the document.

A search box, a connection badge, a ticking clock: they live on the page but they aren't content. One attribute, clay, tells clayjs how any element takes part in saving:

<nav clay="no-save no-watch"> …toolbar, never written to disk… </nav>
<span clay="freeze">12:04:33</span>  <!-- saved as authored, ticking ignored -->
<div clay="no-watch"> …a third-party widget that churns the DOM… </div>
Recipes UI chrome (toolbars, badges) clay="no-save no-watch"
Live or computed text (clocks) clay="freeze"
Third-party embeds clay="no-watch"
Heavy editors, batch the saves clay="no-trigger-autosave"

Six keywords total: no-save, no-snapshot, no-trigger-autosave, no-watch, no-undo, freeze. Full reference on the advanced page.

Get your script tag

What does your file need?

Check what applies. The script tag updates as you go; plugins load only in the browser, only when checked.

Your script tag

CMS setup: add one more tag naming the parts of your page that count as content. The sidebar builds itself from it:

<script data-rules-name="cms" type="application/json">
{
  "title":  ".page-title",
  "intro":  "p.intro",
  "avatar": "img.avatar@src",
  "tags":   "ul.tags li[]"
}
</script>

Each entry is a field: a name, then the selector it edits. Open the sidebar with ?cms=true on the URL. The same tag can power the JSON data API too.

Need more? Reactive templates, JSON data in and out, toasts and dialogs, DOM helpers: those are separate libraries with their own script tags, on Plugins and Advanced.

Where this runs

Your file is yours

It runs on hyperclay.com, on your own machine with Hyperclay Local or htmlclay, or against about 12 lines of your own server code. Download it anytime; it's just HTML.

The fastest way to try a malleable file with zero setup is a free account on hyperclay.com.