HyperclayJS

API Reference

Documentation for all exported functions and utilities.

UI Components

toast

Display a toast notification for success or error messages.

Available as: window.toast, hyperclay.toast
toast(message, messageType)

Parameters

NameTypeDefaultDescription
message string The message to display
messageType string `'success'` Either `'success'` or `'error'`

Returns

void

Example

// Show a success message
toast('Changes saved!', 'success');

// Show an error message
toast('Something went wrong', 'error');

// Success is the default type
toast('Uploaded successfully');

ask

Display a modal with an input field to prompt the user for text input.

Available as: window.ask, hyperclay.ask
ask(promptText, yesCallback, defaultValue, extraContent)

Parameters

NameTypeDefaultDescription
promptText string The question or prompt to display
yesCallback function Called with the input value when user confirms
defaultValue string `''` Pre-filled value in the input field
extraContent string `''` Additional HTML content to display below the input

Returns

Promise<string> — Resolves with the input value, rejects if user closes modal

Example

// Basic usage with async/await
const name = await ask('What is your name?');
console.log('Hello, ' + name);

// With a default value
const title = await ask('Enter title:', null, 'Untitled');

// With callback
ask('Enter your email:', (email) => {
  console.log('Email:', email);
});

// With extra content
ask('Confirm action:', null, '', '<p class="warning">This cannot be undone</p>');

tell

Display an informational modal with a title and optional content paragraphs.

Available as: window.tell, hyperclay.tell
tell(promptText, ...content)

Parameters

NameTypeDefaultDescription
promptText string The title/heading text
...content string[] Additional content paragraphs (variadic)

Returns

Promise<void> — Resolves when user confirms, rejects on close

Example

// Simple message
await tell('Welcome!');

// With additional content
await tell(
  'About This App',
  'This is a collaborative editing platform.',
  'Changes are saved automatically.',
  'Press CMD+S to save manually.'
);

// Informational popup
tell('Tip', 'You can drag and drop items to reorder them.');

snippet

Display a modal with a code snippet and a copy-to-clipboard button.

Available as: window.snippet, hyperclay.snippet
snippet(title, content, extraContent)

Parameters

NameTypeDefaultDescription
title string The modal heading
content string The code/text to display and copy
extraContent string `''` Optional warning or info text below the copy button

Returns

Promise<void> — Resolves when modal is closed

Example

// Show embed code
snippet('Embed Code', '<iframe src="https://example.com"></iframe>');

// With a warning message
snippet(
  'API Key',
  'sk-1234567890abcdef',
  'Keep this key secret. Do not share it publicly.'
);

// Show configuration
const config = JSON.stringify({ theme: 'dark', lang: 'en' }, null, 2);
snippet('Your Settings', config);

themodal

A flexible modal window creation system. Configure and display custom modals with full control over content and behavior.

Available as: window.themodal, hyperclay.themodal
themodal.html = content; themodal.yes = buttonContent; themodal.no = buttonContent; themodal.open(); themodal.close();

Methods

MethodDescription
open() Show the modal
close() Close the modal (triggers onNo callbacks)
onYes(callback) Add callback for confirm action. Return `false` to prevent closing
onNo(callback) Add callback for cancel/close action
onOpen(callback) Add callback for when modal opens

Example

// Basic custom modal
themodal.html = '<h2>Custom Title</h2><p>Your content here</p>';
themodal.yes = 'Confirm';
themodal.no = 'Cancel';

themodal.onYes(() => {
  console.log('User confirmed');
});

themodal.onNo(() => {
  console.log('User cancelled');
});

themodal.open();

// Modal with form validation
themodal.html = '<input class="micromodal__input" type="email" required>';
themodal.yes = 'Submit';

themodal.onYes(() => {
  const email = document.querySelector('.micromodal__input').value;
  if (!email.includes('@')) {
    toast('Invalid email', 'error');
    return false; // Prevent modal from closing
  }
  processEmail(email);
});

themodal.open();

DOM Utilities

All

A lightweight DOM manipulation library with jQuery-like syntax. Select elements, chain methods, handle events with delegation, and more.

Available as: window.All, hyperclay.All
All(selector) All(selector, context) All(element) All(elements)

Parameters

NameTypeDefaultDescription
selector string CSS selector to match elements
context string | Element | Element[] | Document Optional context to limit search scope
element Element Wrap a single DOM element
elements Element[] Wrap an array of DOM elements

Returns

Proxy<Element[]> — A proxied array of elements with chainable methods

Example

// Toggle visibility on all cards
All('.card').classList.toggle('hidden');

// Get values from all inputs
const values = All('input').map(el => el.value);

// Event delegation for dynamic content
All(document).onclick('.dynamic-btn', function() {
  console.log('Button clicked:', this.dataset.id);
});

// Chained operations
All('.notification')
  .classList.add('fade-out')
  .style.opacity = '0';

// Iterate with for...of
for (const el of All('.item')) {
  el.textContent = 'Updated';
}

insertStyles

Insert styles into the document — either an external stylesheet or inline CSS. With a persistent DOM (hyperclay), new styles are inserted first, then duplicates are removed to prevent flickering.

Available as: window.insertStyles, hyperclay.insertStyles
insertStyles(href) insertStyles(name, css)

Parameters

NameTypeDefaultDescription
href string URL of the stylesheet to inject (1-arg form)
name string Unique name for inline styles, used as data-name attribute (2-arg form)
css string CSS content to inject inline (2-arg form)

Returns

HTMLElement

Example

// External stylesheet
insertStyles('/styles/theme.css');

// Load from CDN
insertStyles('https://cdn.example.com/lib.css');

// Inline CSS with a name (for deduplication)
insertStyles('my-theme', `
  .dark-mode { background: #1a1a1a; color: #fff; }
`);

// Safe to call multiple times - old duplicates are removed
insertStyles('/styles/theme.css'); // Replaces previous

getDataFromForm

Extract all form field values as a plain JavaScript object. Works with `<form>` elements or any container with named inputs.

Available as: window.getDataFromForm, hyperclay.getDataFromForm
getDataFromForm(container)

Parameters

NameTypeDefaultDescription
container HTMLFormElement|Element Form element or container with named inputs

Returns

object — Key-value pairs of field names and their values

Example

// From a form element
const form = document.querySelector('form');
const data = getDataFromForm(form);
// { username: 'john', email: 'john@example.com' }

// From any container
const container = document.querySelector('.filter-panel');
const filters = getDataFromForm(container);

// Checkbox handling
// <input type="checkbox" name="tags" value="js" checked>
// <input type="checkbox" name="tags" value="css" checked>
// Result: { tags: ['js', 'css'] }

// Use with fetch
const formData = getDataFromForm(form);
fetch('/api/submit', {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify(formData)
});

onDomReady

Execute a callback when the DOM is ready. If the DOM is already loaded, the callback runs immediately.

Available as: hyperclay.onDomReady
onDomReady(callback)

Parameters

NameTypeDefaultDescription
callback function Function to execute when DOM is ready

Returns

void

Example

// Initialize app when DOM is ready
onDomReady(() => {
  initializeApp();
  setupEventListeners();
});

// Safe to call after page load - runs immediately
onDomReady(() => {
  console.log('This runs right away if DOM is already loaded');
});

onLoad

Execute a callback when the window load event fires (all resources loaded). If already loaded, the callback runs immediately.

Available as: hyperclay.onLoad
onLoad(callback)

Parameters

NameTypeDefaultDescription
callback function Function to execute when window is fully loaded

Returns

void

Example

// Wait for all images and resources to load
onLoad(() => {
  initializeImageGallery();
  calculateLayoutDimensions();
});

// Difference from onDomReady:
// - onDomReady: DOM structure ready, images may still be loading
// - onLoad: Everything loaded including images, fonts, iframes

dom-helpers

Adds convenience methods to all HTML elements for finding and manipulating nearby elements. Built on top of the `nearest` utility.

Methods

MethodDescription
el.cycle(order, attr) Replace element with next element having same attribute
el.cycleAttr(order, setAttr, lookupAttr?) Cycle through attribute values

Example

// Find nearest element with [project] or .project
const projectEl = this.nearest.project;

// Get/set values (smart: uses .value for form elements, attribute otherwise)
const projectName = this.val.project;
this.val.project = "New Name";

// Get/set innerText
const label = this.text.title;
this.text.title = "Updated Title";

// Execute code from an attribute
// If <div sync_out="savePage()"> exists nearby, this runs savePage()
this.exec.sync_out();

// Cycle through elements
// Replaces current element with next element having [variant] attribute
this.cycle(1, 'variant');   // forward
this.cycle(-1, 'variant');  // backward

// Cycle through attribute values
// Sets theme to next value found on any [theme] element
this.cycleAttr(1, 'theme');

// Cycle with different lookup attribute
// Sets color based on values from [option:color] elements
this.cycleAttr(1, 'color', 'option:color');

String Utilities

slugify

Convert text into a URL-friendly slug. Handles accents, spaces, and special characters.

Available as: window.slugify, hyperclay.slugify
slugify(text)

Parameters

NameTypeDefaultDescription
text string The text to convert to a slug

Returns

string — URL-friendly slug

Example

slugify('Hello World');
// 'hello-world'

slugify('Café & Restaurant');
// 'cafe-restaurant'

slugify('  Multiple   Spaces  ');
// 'multiple-spaces'

slugify('Ñoño with Accénts');
// 'nono-with-accents'

// Use for URLs
const title = 'My Blog Post Title!';
const url = `/posts/${slugify(title)}`;
// '/posts/my-blog-post-title'

copyToClipboard

Copy text to the system clipboard.

Available as: hyperclay.copyToClipboard
copyToClipboard(text)

Parameters

NameTypeDefaultDescription
text string The text to copy to clipboard

Returns

void

Example

// Copy a URL
copyToClipboard('https://example.com/share/123');

// Copy with user feedback
copyToClipboard(embedCode);
toast('Copied to clipboard!');

// Copy from an element
const code = document.querySelector('pre').textContent;
copyToClipboard(code);

query

An object containing parsed URL query parameters from the current page URL.

Available as: window.query, hyperclay.query
query

Example

// URL: https://example.com/page?name=john&page=2&active=true

query.name;    // 'john'
query.page;    // '2'
query.active;  // 'true'

// Check if parameter exists
if (query.debug) {
  enableDebugMode();
}

// Use with defaults
const page = query.page || '1';
const sort = query.sort || 'date';

// Destructure parameters
const { name, page, sort = 'date' } = query;

Utilities

throttle

Limit how often a function can be called. The function executes at most once per specified delay period.

Available as: hyperclay.throttle
throttle(callback, delay, executeFirst)

Parameters

NameTypeDefaultDescription
callback function The function to throttle
delay number Minimum time between calls in milliseconds
executeFirst boolean `true` Execute immediately on first call

Returns

function — Throttled version of the callback

Example

// Throttle scroll handler to once per 100ms
const handleScroll = throttle(() => {
  updateScrollPosition();
}, 100);

window.addEventListener('scroll', handleScroll);

// Throttle resize handler
const handleResize = throttle(() => {
  recalculateLayout();
}, 200);

window.addEventListener('resize', handleResize);

// Don't execute immediately on first call
const lazyUpdate = throttle(updateUI, 500, false);

debounce

Delay function execution until after a period of inactivity. The timer resets each time the function is called.

Available as: hyperclay.debounce
debounce(callback, delay)

Parameters

NameTypeDefaultDescription
callback function The function to debounce
delay number Wait time in milliseconds after last call

Returns

function — Debounced version of the callback

Example

// Debounce search input
const searchInput = document.querySelector('#search');
const handleSearch = debounce((query) => {
  fetchSearchResults(query);
}, 300);

searchInput.addEventListener('input', (e) => {
  handleSearch(e.target.value);
});

// Debounce window resize
const handleResize = debounce(() => {
  recalculateLayout();
}, 250);

window.addEventListener('resize', handleResize);

// Auto-save after user stops typing
const autoSave = debounce(() => {
  saveDraft();
}, 1000);

cacheBust

Cache-bust an element's href or src attribute by adding or updating a version query parameter. Useful for reloading stylesheets or scripts after dynamic changes.

Available as: window.cacheBust, hyperclay.cacheBust
cacheBust(element)

Parameters

NameTypeDefaultDescription
element HTMLElement Element with href or src attribute to cache-bust

Returns

void

Example

// Cache-bust a stylesheet link
const link = document.querySelector('link[rel="stylesheet"]');
cacheBust(link);
// href="/styles.css" becomes "/styles.css?v=1702847291234"

// Cache-bust an image
const img = document.querySelector('img');
cacheBust(img);
// src="/photo.jpg?v=123" becomes "/photo.jpg?v=1702847291234"

// Use with onaftersave to reload Tailwind CSS after save
// <link href="/tailwindcss/mysite.css" onaftersave="cacheBust(this)">

Mutation

A lightweight wrapper around MutationObserver for watching DOM changes. Provides methods to observe element additions, removals, and attribute changes.

Available as: window.Mutation, hyperclay.Mutation
Mutation.onAnyChange(options, callback) Mutation.onAddOrRemove(options, callback) Mutation.onAddElement(options, callback) Mutation.onRemoveElement(options, callback) Mutation.onAttribute(options, callback)

Parameters

NameTypeDefaultDescription
options object `{}` Configuration options
options.debounce number `0` Debounce delay in milliseconds
options.selectorFilter string|function Filter changes to matching elements
options.omitChangeDetails boolean `false` Call callback without change data
callback function Called with array of changes

Methods

MethodDescription
onAnyChange Watch all DOM changes
onAddOrRemove Watch element additions and removals
onAddElement Watch only element additions
onRemoveElement Watch only element removals
onAttribute Watch attribute changes

Returns

function — Call to stop observing

Example

// Watch for any DOM changes (debounced)
const unsubscribe = Mutation.onAnyChange({ debounce: 200 }, (changes) => {
  changes.forEach(change => {
    console.log(change.type, change.element);
  });
});

// Watch for new elements matching a selector
Mutation.onAddElement({ selectorFilter: '.card' }, (changes) => {
  changes.forEach(({ element }) => {
    initializeCard(element);
  });
});

// Watch for attribute changes
Mutation.onAttribute({ debounce: 100 }, (changes) => {
  changes.forEach(({ element, attribute, oldValue, newValue }) => {
    console.log(`${attribute} changed from ${oldValue} to ${newValue}`);
  });
});

// Simple debounced callback without change details
Mutation.onAnyChange({ debounce: 500, omitChangeDetails: true }, () => {
  console.log('DOM changed');
});

// Stop observing
unsubscribe();

nearest

Search for elements matching a CSS selector by exploring outward from a starting point. Unlike `element.closest()` which only searches ancestors, this explores siblings, cousins, and nearby elements in visual proximity.

Available as: window.nearest, hyperclay.nearest
nearest(startElem, selector, elementFoundReturnValue)

Parameters

NameTypeDefaultDescription
startElem Element Starting element for the search
selector string CSS selector to match
elementFoundReturnValue function `x => x` Transform function for the found element

Returns

Element | any | null — Found element (or transformed value), or `null` if not found

Example

// Find the nearest button
const btn = nearest(clickedElement, 'button');

// Find nearest input field from a label
const input = nearest(label, 'input');

// Find nearest and get its value
const value = nearest(el, '[data-value]', el => el.dataset.value);

// Find related UI elements
const card = nearest(deleteBtn, '.card');
card.remove();

// Find next input in reading order
const nextInput = nearest(currentInput, 'input:not(:disabled)');
nextInput?.focus();

Communication

sendMessage

Send form data or a custom object to the `/message` endpoint. Automatically collects form data, includes behavior tracking, and handles success/error toasts.

Available as: hyperclay.sendMessage
sendMessage(eventOrObj, successMessage, callback)

Parameters

NameTypeDefaultDescription
eventOrObj Event|object Form submit event, click event, or data object to send
successMessage string `'Successfully sent'` Toast message on success
callback function Called with response data on success

Returns

Promise<object> — Resolves with server response, rejects on error

Example

// Handle form submission
document.querySelector('form').onsubmit = (e) => {
  sendMessage(e, 'Message sent!');
};

// Event outside a form (sends behavior data only)
document.querySelector('#contact-btn').onclick = (e) => {
  sendMessage(e, 'Contact request sent!');
};

// Send custom data object
sendMessage({
  name: 'John',
  email: 'john@example.com'
}, 'Contact form submitted');

// With async/await
try {
  const result = await sendMessage(formEvent);
  redirectToThankYou();
} catch (error) {
  console.error('Failed:', error);
}

uploadFile

Upload a file to the `/upload` endpoint with progress toasts and automatic clipboard copy of the resulting URL.

Available as: hyperclay.uploadFile
uploadFile(eventOrFile, callback, extraData)

Parameters

NameTypeDefaultDescription
eventOrFile Event|File File input change event or File object
callback function `() => {}` Called with response on success
extraData object `{}` Additional data to include in the request

Returns

Promise<object> — Resolves with server response containing URLs

Example

// Handle file input
document.querySelector('input[type="file"]').onchange = (e) => {
  uploadFile(e, (response) => {
    console.log('Uploaded to:', response.urls);
  });
};

// Upload a File object directly
const file = new File(['content'], 'test.txt', { type: 'text/plain' });
uploadFile(file);

// With extra metadata
uploadFile(event, null, {
  folder: 'images',
  public: true
});

// Progress is shown automatically via toasts:
// "10% uploaded" → "50% uploaded" → "80% uploaded" → "Uploaded! URL copied"

createFile

Create and upload a file from text content. Automatically detects content type (HTML, CSS, JS, JSON, etc.) and adjusts the file extension.

Available as: hyperclay.createFile
createFile(eventOrData) createFile(fileName, fileBody)

Returns

Promise<object> — Resolves with server response containing URLs

Example

// From form submission
document.querySelector('#create-file-form').onsubmit = (e) => {
  createFile(e);
};

// From object
createFile({
  fileName: 'styles.css',
  fileBody: '.container { max-width: 1200px; }'
});

// From arguments
createFile('config.json', JSON.stringify({ theme: 'dark' }));

// Content type auto-detection:
// - HTML content → .html
// - CSS content → .css
// - JavaScript → .js
// - Valid JSON → .json
// - Unknown → .txt

uploadFileBasic

Upload a file with custom progress, completion, and error callbacks. A lower-level alternative to `uploadFile` without progress toasts (HTTP errors still trigger toast notifications).

Available as: hyperclay.uploadFileBasic
uploadFileBasic(eventOrFile, options)

Parameters

NameTypeDefaultDescription
eventOrFile Event|File File input change event or File object
options object `{}` Callback options
options.onProgress function `() => {}` Called with percent complete (0-100)
options.onComplete function `() => {}` Called with response on success
options.onError function `() => {}` Called with error on failure

Returns

Promise<object> — Resolves with server response, rejects on error

Example

// With custom progress UI
uploadFileBasic(fileInput.files[0], {
  onProgress: (percent) => {
    progressBar.style.width = percent + '%';
    progressText.textContent = percent + '%';
  },
  onComplete: (response) => {
    progressBar.classList.add('complete');
    showSuccessMessage(response.urls[0]);
  },
  onError: (error) => {
    progressBar.classList.add('error');
    showErrorMessage(error.message);
  }
});

// Minimal usage
uploadFileBasic(event, {
  onComplete: (res) => console.log('Done:', res.urls)
});

// With async/await
try {
  const result = await uploadFileBasic(file, {
    onProgress: p => console.log(p + '%')
  });
  console.log('Uploaded:', result);
} catch (err) {
  console.error('Failed:', err);
}

live-sync

Real-time DOM sync across browsers via SSE. When one user saves, all connected browsers see the changes instantly via HyperMorph. Works with Hyperclay Local app.

Parameters

NameTypeDefaultDescription
file string Site identifier to sync (e.g. `'index'`, `'about'`). Auto-detected from URL pathname if omitted

Methods

MethodDescription
liveSync.start(file?) Start syncing. Auto-detects file from URL if not provided
liveSync.stop() Stop syncing and clean up SSE connection

Example

// Auto-start (default behavior when module is loaded)
await import('hyperclay.js?features=live-sync');

// Manual control
import liveSync from 'hyperclay.js/live-sync';
liveSync.start('my-page');

// Callbacks
liveSync.onConnect = () => console.log('Connected');
liveSync.onDisconnect = () => console.log('Disconnected');
liveSync.onUpdate = ({ html, sender }) => console.log('Update from', sender);
liveSync.onError = (err) => console.error(err);

// Stop syncing
liveSync.stop();

Event Attributes

onclickaway

Execute code when a click occurs outside the element.

Example

<!-- Close dropdown when clicking outside -->
<div class="dropdown" onclickaway="this.classList.add('hidden')">
  <button>Menu</button>
  <ul class="dropdown-items">
    <li>Option 1</li>
    <li>Option 2</li>
  </ul>
</div>

<!-- Close modal when clicking backdrop -->
<div class="modal-backdrop" onclickaway="All.modal.remove()">
  <div class="modal">Modal content</div>
</div>

onclickchildren

Execute code when any direct child of the element is clicked.

Example

<!-- Hide menu when any menu item is clicked -->
<div menu class="dropdown" onclickchildren="All.menu.classList.add('hidden')">
  <button>Option 1</button>
  <button>Option 2</button>
  <button>Option 3</button>
</div>

<!-- Handle click on list items, even with nested content -->
<ul onclickchildren="console.log('Clicked:', this.dataset.id)">
  <li data-id="1"><span>Item 1</span></li>
  <li data-id="2"><span>Item 2</span></li>
  <li data-id="3"><span>Item 3</span></li>
</ul>

<!-- Delegate actions based on which child was clicked -->
<nav onclickchildren="this.classList.add('active'); All('nav > *').not(this).classList.remove('active')">
  <a href="#home">Home</a>
  <a href="#about">About</a>
  <a href="#contact">Contact</a>
</nav>

onclone

Execute code when an element is cloned via `cloneNode()`.

Example

<!-- Generate unique IDs for cloned elements -->
<template id="item-template">
  <div class="item" onclone="this.id = 'item-' + Date.now()">
    <input type="text">
  </div>
</template>

<!-- Clear input values in cloned forms -->
<form onclone="All('input', this).value = ''">
  <input type="text" value="default">
  <button type="submit">Submit</button>
</form>

<!-- Initialize cloned components -->
<div class="widget" onclone="this.dataset.initialized = 'false'; initWidget(this)">
  Widget content
</div>

onmutation

Execute code when the element or its descendants change.

Example

<!-- Update count when list changes -->
<div onmutation="All.count.textContent = this.children.length">
  <ul>
    <li>Item 1</li>
    <li>Item 2</li>
  </ul>
</div>
<span count>2</span>

<!-- Auto-save when content is edited -->
<div contenteditable onmutation="await saveContent(this.innerHTML)">
  Edit this content...
</div>

<!-- Sync state with DOM changes -->
<div class="todo-list" onmutation="updateTodoState(this)">
  <div class="todo-item">Task 1</div>
  <div class="todo-item">Task 2</div>
</div>

onpagemutation

Execute code when any element on the page changes. Also available as `onglobalmutation`.

Example

<!-- Live counter that updates when items change -->
<span onpagemutation="this.textContent = All('li').length">0</span>
<ul>
  <li>Item 1</li>
  <li>Item 2</li>
</ul>

<!-- Update totals when table data changes -->
<tfoot>
  <tr>
    <td onglobalmutation="this.textContent = calculateTotal()">$0</td>
  </tr>
</tfoot>

<!-- Sync badge count with notifications -->
<span class="badge" onpagemutation="this.textContent = All('.notification:not(.read)').length">
  0
</span>

onrender

Execute code once when an element is rendered (added to the DOM).

Example

<!-- Initialize a component on render -->
<div class="chart" onrender="initChart(this)">
  Loading chart...
</div>

<!-- Fetch and display data -->
<div onrender="this.innerHTML = await fetchUserProfile()">
  Loading profile...
</div>

<!-- Set initial state -->
<input type="text" onrender="this.value = localStorage.getItem('draft') || ''">

<!-- Focus first input in dynamically added forms -->
<form onrender="this.querySelector('input')?.focus()">
  <input type="text" placeholder="Name">
  <input type="email" placeholder="Email">
</form>

<!-- Load content lazily -->
<div class="lazy-section" onrender="
  const content = await fetch(this.dataset.src).then(r => r.text());
  this.innerHTML = content;
" data-src="/partials/sidebar.html">
  Loading...
</div>

Custom Attributes

persist

Sync form input values to the saved/synced page snapshot.

Example

<!-- Text input persists its value -->
<input type="text" persist placeholder="Your name">

<!-- Checkbox persists checked state -->
<label>
  <input type="checkbox" persist> Remember me
</label>

<!-- Textarea persists content -->
<textarea persist placeholder="Notes..."></textarea>

<!-- Select persists selection (including multiple) -->
<select persist>
  <option value="a">Option A</option>
  <option value="b">Option B</option>
</select>

<select multiple persist>
  <option value="1">One</option>
  <option value="2">Two</option>
  <option value="3">Three</option>
</select>

onaftersave

Execute code after a successful page save.

Example

<!-- Cache-bust a stylesheet after save -->
<link href="styles.css" onaftersave="cacheBust(this)">

<!-- Show save status -->
<span onaftersave="this.textContent = event.detail.msg">
  Not saved
</span>

<!-- Update timestamp -->
<span onaftersave="this.textContent = new Date(event.detail.timestamp).toLocaleTimeString()">
  --:--:--
</span>

<!-- Trigger custom logic -->
<div onaftersave="analytics.track('page_saved')"></div>

<!-- Refresh preview after save -->
<iframe src="/preview" onaftersave="this.src = this.src"></iframe>

prevent-enter

Prevent the Enter key from creating newlines in an element.

Example

<!-- Single-line contenteditable heading -->
<h1 contenteditable prevent-enter>Edit this title</h1>

<!-- Single-line textarea (alternative to input) -->
<textarea prevent-enter placeholder="Single line only"></textarea>

<!-- Prevent Enter in a specific area -->
<div prevent-enter>
  <span contenteditable>Name</span>
  <span contenteditable>Email</span>
</div>

autosize

Automatically grow textarea height to fit content.

Example

<!-- Basic auto-growing textarea -->
<textarea autosize placeholder="Start typing..."></textarea>

<!-- With minimum height via CSS -->
<textarea autosize style="min-height: 100px;"></textarea>

<!-- Notes field that expands -->
<label>
  Notes
  <textarea autosize name="notes"></textarea>
</label>

sortable

Enable drag-and-drop sorting on child elements.

Example

<!-- Basic sortable list -->
<ul sortable>
  <li>Drag me</li>
  <li>And me</li>
  <li>Me too</li>
</ul>

<!-- With drag handle -->
<ul sortable>
  <li>
    <span sortable-handle>⋮⋮</span>
    Item with handle
  </li>
  <li>
    <span sortable-handle>⋮⋮</span>
    Another item
  </li>
</ul>

<!-- Grouped lists (drag between) -->
<div style="display: flex; gap: 1rem;">
  <ul sortable="shared">
    <li>List A - Item 1</li>
    <li>List A - Item 2</li>
  </ul>
  <ul sortable="shared">
    <li>List B - Item 1</li>
    <li>List B - Item 2</li>
  </ul>
</div>

<!-- With callbacks -->
<ul sortable onsorted="console.log('Reordered!'); savePage()">
  <li>Item 1</li>
  <li>Item 2</li>
</ul>

<ul sortable onsorting="this.classList.add('dragging')"
             onsorted="this.classList.remove('dragging')">
  <li>Item 1</li>
  <li>Item 2</li>
</ul>

option-visibility

Show or hide elements based on ancestor attribute values.

Example

<!-- Show/hide based on edit mode -->
<html editmode="true">
  <body>
    <button option:editmode="true">Edit</button>   <!-- visible -->
    <button option:editmode="false">View</button>  <!-- hidden -->
  </body>
</html>

<!-- Theme-based visibility -->
<html theme="dark">
  <img option:theme="light" src="logo-dark.png">   <!-- hidden -->
  <img option:theme="dark" src="logo-light.png">   <!-- visible -->
</html>

<!-- Role-based UI -->
<div role="admin">
  <button option:role="admin">Delete All</button>  <!-- visible -->
  <button option:role="user">My Items</button>     <!-- hidden -->
</div>

<!-- Nested contexts -->
<div editmode="true">
  <div editmode="false">
    <span option:editmode="true">A</span>   <!-- hidden (nearest ancestor is false) -->
    <span option:editmode="false">B</span>  <!-- visible -->
  </div>
</div>

Save/Edit Features

edit-mode

Provides edit mode detection and toggling for page editing.

Methods

MethodDescription
toggleEditMode() Toggle edit mode on/off (reloads page)

Example

// Check if in edit mode
if (hyperclay.isEditMode) {
  // Show edit UI
}

// Check if user owns the resource
if (hyperclay.isOwner) {
  // Show owner-only controls
}

// Toggle edit mode (reloads page with ?editmode=true/false)
hyperclay.toggleEditMode();

edit-mode-helpers

Sets `editmode` and `pageowner` attributes on `<html>` for CSS-based conditional styling.

Example

<!-- In edit mode as owner -->
<html editmode="true" pageowner="true">
  ...
</html>

<!-- Viewing as non-owner -->
<html editmode="false" pageowner="false">
  ...
</html>

save-system

Manual save with change detection, state management, keyboard shortcuts, and save button support.

Methods

MethodDescription
savePage(callback?) Save page if content changed
savePageThrottled(callback?) Throttled save for auto-save use
replacePageWith(url) Fetch HTML from URL and save it
beforeSave(fn) Register a hook to modify content before saving
getPageContents() Get current page HTML as string

Example

// Manual save
hyperclay.savePage();

// Save with callback
hyperclay.savePage(({msg, msgType}) => {
  if (msgType === 'error') {
    console.error('Save failed:', msg);
  }
});

// Register before-save hook
hyperclay.beforeSave((clone) => {
  // Modify the snapshot clone before saving
  clone.querySelectorAll('.temp').forEach(el => el.remove());
});

// Replace page with template
hyperclay.replacePageWith('/templates/blog.html');

save-toast

Shows toast notifications for save lifecycle events. No configuration needed.

Methods

MethodDescription
hyperclay:save-saved Green success toast with "Saved"
hyperclay:save-error Red error toast with "Failed to save"
hyperclay:save-offline Red error toast with "No internet connection"

Example

<!-- Basic setup -->
<script src="hyperclay.js?save-system,save-toast"></script>

<!-- Full save stack with auto-save and notifications -->
<script src="hyperclay.js?save-system,autosave,save-toast"></script>

unsaved-warning

Warns users before leaving the page if there are unsaved changes. On `beforeunload`, compares current page content to last saved content and shows the browser's native dialog if different.

Methods

MethodDescription
beforeunload Fires automatically when user tries to leave with unsaved changes
getPageContents() Used internally to compare current vs saved state

Example

<!-- Basic setup -->
<script src="hyperclay.js?save-system,unsaved-warning"></script>

<!-- Full save stack -->
<script src="hyperclay.js?save-system,autosave,unsaved-warning,save-toast"></script>

autosave

Automatically saves the page when DOM changes are detected. Debounces at 3.3s after last change, throttles saves to max once per 1.2s. Only saves if content actually changed.

Methods

MethodDescription
MutationObserver Watches for any DOM mutations automatically
savePageThrottled() Called internally after debounce completes

Example

<!-- Basic auto-save -->
<script src="hyperclay.js?save-system,autosave"></script>

<!-- With toast notifications -->
<script src="hyperclay.js?save-system,autosave,save-toast"></script>

<!-- Full save stack -->
<script src="hyperclay.js?save-system,autosave,unsaved-warning,save-toast"></script>

tailwind-inject

Injects a Tailwind CSS stylesheet for the current resource and cache-busts it on every save. Edit-mode only.

Parameters

NameTypeDefaultDescription
currentResource cookie Read from `currentResource` cookie. Determines the CSS path: `/tailwindcss/{currentResource}.css`

Example

<!-- Include with other save features -->
<script type="module">
  await import('hyperclay.js?features=tailwind-inject,save-system');
</script>

<!-- Or use the everything preset -->
<script type="module">
  await import('hyperclay.js?preset=everything');
</script>