Core Concepts
We encourage thinking differently about how you build a hyperclay app than you would a regular app.
The idea behind Hyperclay is that you have a new kind of document. It’s a web document, but it’s dynamic, portable, fungible. You can pass it around to your friends. Or host it anywhere. It’s portable, light, easy-to-think about.
Many of these patterns are handled automatically by HyperclayJS — the JavaScript library that turns a plain HTML file into a self-saving, editable app.
Explore HyperclayJS →Traditional Web App vs Hyperclay
| Traditional Web App | Hyperclay App |
|---|---|
| Separate frontend/backend | Single HTML file |
| Database queries | DOM as database |
| API endpoints | Direct DOM manipulation |
| Build process | No build needed |
| Multiple files | Self-contained document |
| Complex state management | DOM attributes as state |
Regular apps have lots of moving pieces, lots of parts to integrate together from different areas of the stack. For this reason, principles like DRY and separation of concerns matter a lot more.
But with Hyperclay, it’s the opposite. You want to mush everything together. Keep locality of concern. If you look at a piece of your HTML later, even just a small piece, you should understand how it functions in itself and as part of the wider project within a few seconds.
Example: Traditional vs Hyperclay Toggle
Traditional approach:
// script.js
document.getElementById('toggle').addEventListener('click', () => {
fetch('/api/toggle-setting')
.then(res => res.json())
.then(data => updateUI(data));
});
// Separate API endpoint, database update, etc.Hyperclay approach:
<div settings-panel="closed">
<button onclick="this.closest('[settings-panel]').setAttribute('settings-panel',
this.closest('[settings-panel]').getAttribute('settings-panel') === 'open' ? 'closed' : 'open')">
Toggle Settings
</button>
<div option:settings-panel="open">Settings content here...</div>
</div>Your document should look and behave simpler than an app, where behaviors and UI and state are so separate and so different from each other. A hyperclay document should feel more like a state machine. Something flat and grokable. Declarative is the word I’m looking for. It’s not like a regular web app, with a bunch of GOTO-like syntax, like AJAX calls that trigger backend functionality, that sends a response, that ends up in some other piece of higher-level state in your app. No, it’s just a DOM tree. You change state somewhere, just once, and the behavior and UI change appropriately. It’s no big deal.
DOM as Database
In traditional web apps, data lives in a separate database. In Hyperclay, the DOM itself is your database.
Traditional approach:
// Data in JavaScript
let tasks = [{id: 1, text: "Buy milk", done: false}];
// Sync to DOM
function renderTasks() {
tasksContainer.innerHTML = tasks.map(t =>
`<div>${t.text}</div>`
).join('');
}Hyperclay approach:
<!-- Data lives directly in the DOM -->
<div class="task" data-done="false">Buy milk</div>The DOM element is the single source of truth. No synchronization needed.
For most cases, the DOM will act as a natural database, which means you don’t have to worry about anything. The user will edit the text of an element and that element will not be modified. The user will move an element somewhere else, and now that element will be permanently moved.
The fun thing about using the DOM as your database is the DOM’s tree structure. Since it has cascading nodes and branches, each level of those nodes and branches is a natural component. It only needs to be tagged to create a natural encapsulation of functionality and state.
Another neat feature is the way attributes and their values can be used to simultaneously store behavior, information, and appearance. This makes it a state machine and a hierarchical declarative UI/UX model. This is a powerful combination, allowing you to encode a lot of cascading options and functionality in the toggle of a single attribute value.
Using JSON for Complex Data
But for special cases, you may want to store data as JSON or another special format. In this case, I’d recommend using a <script type="application/json"> tag and using it as a database you can read, write, and update.
<script type="application/json" id="app-data">
{
"users": [],
"settings": {
"theme": "light",
"notifications": true
}
}
</script>
<script>
// Read data
const data = JSON.parse(document.getElementById('app-data').textContent);
// Update data
function updateTheme(theme) {
data.settings.theme = theme;
document.getElementById('app-data').textContent = JSON.stringify(data, null, 2);
}
</script>Malleable Documents
Hyperclay documents can modify and save themselves. This isn’t just editing content—it’s the document rewriting its own source code.
How it works:
- User interacts with the page
- JavaScript modifies the DOM
- Save captures the entire DOM state
- The modified HTML becomes the new document
Example self-modification:
<button onclick="this.nearest.section.after(this.nearest.section.cloneNode(true));">Add Section</button>When saved, the new section becomes part of the permanent document.
View vs Edit Mode
Every Hyperclay document operates in two distinct modes:
View Mode
- Default state for all visitors
- Interactive but read-only
- No editing controls visible
- Forms and buttons work normally
Edit Mode
- Available only to document owners
- Editing controls appear
- Elements become modifiable
- Save functionality enabled
Mode-specific elements:
<!-- Only visible in edit mode -->
<button option:editmode="true" trigger-save>Save Changes</button>
<!-- Editable only in edit mode -->
<h1 edit-mode-contenteditable>Page Title</h1>It’s much preferable to hide admin controls instead of removing them from the page entirely. This gives us less to worry about dynamically adding when the page loads and sticks to the model of a hypermedia application, where state and associated functionality is fully loaded into the page at all times. For conditionally showing/hiding UI, we have the excellent option: attributes.
Using option: Attributes
<!-- Show different content based on user role -->
<div user-role="guest">
<div option:user-role="guest">
<button onclick="this.closest('[user-role]').setAttribute('user-role', 'admin')">
Login as Admin
</button>
</div>
<div option:user-role="admin">
<h3>Admin Panel</h3>
<button onclick="this.closest('[user-role]').setAttribute('user-role', 'guest')">
Logout
</button>
</div>
</div>Save-Strip-Restore Cycle
The three-phase cycle that enables clean separation between editing and viewing:
1. Save Phase
When you trigger save:
onbeforesavecallbacks fire for cleanup- Elements with
edit-mode-resourceare removed - Attributes prefixed with
edit-mode-are stripped - The cleaned DOM is serialized to HTML
2. Strip Phase
The server stores the cleaned HTML:
- No editing interfaces remain
- No admin-only code exists
- Just the pure application
3. Restore Phase
When an editor loads the page:
- Edit permissions are detected
edit-mode-attributes are re-enabled- Hidden admin interfaces appear
- Save functionality activates
If you need to do more complex operations to prepare a page for viewing by anonymous web visitors, you can use onbeforesave attributes. These are powerful because you can run any arbitrary JS inside them you can think of.
Locality of Concern
Unlike traditional web development that separates HTML, CSS, and JavaScript into different files, Hyperclay embraces locality—keeping related code together.
Everything in one place:
<div class="counter">
<style>
.counter {
border: 1px solid #ccc;
padding: 20px;
text-align: center;
}
</style>
<h3>Counter: <span>0</span></h3>
<button onclick="this.previousElementSibling.querySelector('span').textContent++">
Increment
</button>
</div>Structure, styling, and behavior unite in a single, understandable unit.
Don’t Be Afraid to Inline Code
When you want people to administrate your page, I highly recommend using the onclick, oninput, onsubmit attributes. Inline event handlers are great at encapsulating bite-size pieces of functionality and they live local to where the activity is happening. It’s easy to design an app that’s simple, where everything can fit into your head.
<button onclick="this.textContent = 'Clicked!'">Click me</button>
<input oninput="this.nextElementSibling.textContent = this.value">
<select onchange="document.body.className = this.value">When everything is visible at the point of use, there’s no need to hunt through files to understand behavior.
all.js
I even created a custom jQuery alternative just for this purpose: to be syntactically concise enough to fit into inline event attributes. You can also just use jQuery, it’s a great fit for hyperclay, but you have a second option as well: all.js.
<!-- Toggle all panels at once -->
<button onclick="all.panel.classList.toggle('expanded')">Toggle All Panels</button>
<!-- Filter and remove draft items -->
<button onclick="all.item.filter(el => el.dataset.status === 'draft').remove()">
Clear Drafts
</button>
<!-- Update multiple elements -->
<button onclick="all.price.forEach(el => el.textContent = '$' + el.textContent)">
Format Prices
</button>Template-Clone Pattern
Create dynamic lists by cloning template elements rather than generating HTML strings.
Template element (hidden):
<ul id="tasks">
<!-- First child is the template -->
<li class="task hidden" onclone="this.classList.remove('hidden')">
<input type="checkbox" persist>
<span contenteditable>New task</span>
<button onclick="this.nearest.task.remove()">Delete</button>
</li>
</ul>Add button:
<button onclick="this.nearest.task.after(this.nearest.task.cloneNode(true));">Add Task</button>The template provides structure. Cloning creates instances. The DOM provides its own component system.
Event Delegation
Since content can be added dynamically, use event delegation to handle events on future elements:
<script>
// Attach to parent, not individual elements
document.addEventListener('click', e => {
// Complete task
if (e.target.matches('.task-complete')) {
e.target.closest('.task').dataset.status = 'done';
}
// Delete task
if (e.target.matches('.task-delete')) {
e.target.closest('.task').remove();
}
});
</script>New elements automatically inherit behavior based on their classes and position in the DOM.
Progressive Enhancement
Start simple, add complexity only when needed:
<!-- Level 1: Basic HTML -->
<h1>My App</h1>
<!-- Level 2: Make it editable -->
<h1 contenteditable>My App</h1>
<!-- Level 3: Only editable for owners -->
<h1 edit-mode-contenteditable>My App</h1>Each attribute adds one specific capability. Build up functionality incrementally.
Single File Philosophy
Everything lives in one HTML file:
- Markup: The structure
- Styles: The appearance
- Scripts: The behavior
- Data: The state
- Editor: The development environment
When everything is in one place, you can understand the entire application at once. No hidden dependencies. No configuration files. No build process.
Your HTML file is simultaneously:
- The source code
- The deployed application
- The development environment
- The data storage
To build an app on Hyperclay, you build your front-end how you’d normally build it, using whatever vanilla JS libraries you want. Add all the admin controls and editable areas you want, but try to keep them supplemental to the page, so removing or hiding them won’t affect the flow of the page that much.
Explore HyperclayJS →