<!-- clayjs: malleable html files · https://clayjs.com/ · generated from the page; every page has one at its path plus .md -->

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 saved in your browser

### My notes

This note is the whole app. No database, no build step.

Everything you type here is saved into the file itself.

Click the heading or the note and type. Select text to format it. Then save, refresh the page, and it's still there.

```html
<!DOCTYPE html>
<html>
  <body>
    <h1 editable>My notes</h1>
    <button onclick="clay.save()">Save</button>
    <script src="https://clayjs.com/v1/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](https://clayjs.com/#save) [Get your script tag](https://clayjs.com/#configure)

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:

```html
<script src="https://clayjs.com/v1/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 as plain text to `/_/save` on the server it was loaded from, identifies the document with the `Document-URL` header, includes same-origin cookies, and treats the HTTP status as authoritative. A 2xx response accepts the save and normally includes JSON such as `{ "msg": "Saved" }`; any non-2xx refuses or fails it, even when a proxy returns HTML instead of JSON. That's the whole core protocol. Anything that implements that one endpoint can host malleable files:

| Server | What it is |
| --- | --- |
| **[hyperclay.com](https://hyperclay.com/)** | Hosted platform: accounts, versions, sharing |
| **[Hyperclay Local](https://hyperclaylocal.com/)** | Desktop app that serves a folder on your machine |
| **[HTML Clay](https://htmlclay.com/)** | Desktop app: double-click a .htmlclay file and it just saves |
| **Your own** | About 20 lines of Express. [Endpoint spec](https://clayjs.com/docs/endpoint). On your own server you also arm [edit mode](https://clayjs.com/docs/edit-mode) yourself, with one line before the script tag. |

## 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:

```css
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 saved in your browser

Try both outcomes:

The chip is yours: restyle it, move it, delete it. One caveat: generated content is decoration, so a screen reader never announces it. If the save state matters to everyone, put a real element on the page and write it from the events instead, which is what the demo chips above do:

```html
<span id="status" role="status" clay="no-save no-watch no-snapshot"></span>
<script>
  status.textContent = "";
  document.addEventListener("clay:save-saved", () => status.textContent = "Saved");
  document.addEventListener("clay:save-error", () => status.textContent = "Couldn't save");
</script>
```

The `clay="no-save no-watch no-snapshot"` is the important part: this element is written _after_ the save baseline is taken, so without it every successful save would leave the page looking unsaved again.

**Don't want to touch CSS?** Check _“a built-in save indicator”_ in the [configurator](https://clayjs.com/#configure) 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:

```javascript
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.

## 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:

```html
<p editable>Meeting notes for Tuesday…</p>
<input name="title" persist>
<textarea name="draft" persist></textarea>
```

meeting.html saved in your browser

Meeting notes for Tuesday…

Edit both, save, then reload: everything survives. Reload _without_ saving and your changes are gone.

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
<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](https://clayjs.com/#configure). 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: it holds a connection open, and you don't need that until you have this problem.

plan.html · your laptop simulated

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.

```html
<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:

```html
<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"  
Self-rendering UI (filter bars) → clay="no-dirty"

Seven keywords total: `no-save`, `no-snapshot`, `no-trigger-autosave`, `no-dirty`, `no-watch`, `no-undo`, `freeze`. Full reference on the [advanced page](https://clayjs.com/advanced#regions).

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.

SavingThe core. Always on. Rich text editingThe `editable` attribute: bold, lists, links. On by default. A built-in save indicatorA small ready-made status chip, if you'd rather not style your own. Sync edits across open copiesLive sync between tabs, devices, and collaborators. Drag items to reorderThe `sortable` attribute for lists and boards. Undo / redo⌘Z that understands the whole document, not just text fields. AI editingSelect text, press ⌘J, and ask your AI agent for a change. Needs HTML Clay or Hyperclay Local. A content sidebar (CMS)Edit-in-place panel for the file's content. Needs a two-line setup, shown below.

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:

```html
<script data-rules-name="cms" data-rules-version="1" 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](https://clayjs.com/plugins) 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](https://clayjs.com/plugins) and [Advanced](https://clayjs.com/advanced).

Where this runs

## Your file is yours

It runs on [hyperclay.com](https://hyperclay.com/), on your own machine with [Hyperclay Local](https://hyperclaylocal.com/) or [HTML Clay](https://htmlclay.com/), or against [about 20 lines of your own server code](https://clayjs.com/docs/endpoint). 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](https://hyperclay.com/).
