joby/smol-frame

A lightweight and unopinionated alternative to Javascript libraries like HTMX or Turbo, focused on progressively-enhancing websites that would work fine without it.

Maintainers

Package info

github.com/joby-lol/smol-frame

Language:JavaScript

pkg:composer/joby/smol-frame

Transparency log

Statistics

Installs: 4

Dependents: 0

Suggesters: 0

Stars: 0

Open Issues: 0

dev-main 2026-08-13 18:54 UTC

This package is auto-updated.

Last update: 2026-08-13 18:54:23 UTC


README

A zero-dependency, ultra-lightweight (~1.3KB gzipped) Progressive Enhancement engine for server-rendered web applications.

smolFrame intercepts standard links and form submissions, fetches full HTML documents via fetch, parses out target DOM fragments, and swaps them seamlessly without breaking page reloads, SEO, or browser history.

Philosophy

  • Progressive Enhancement First: Your web application works 100% out of the box with JavaScript disabled using standard HTTP links, HTML forms, and server redirects.
  • Zero Double-Life Controllers: Server endpoints render standard, full HTML pages. smolFrame extracts the relevant container fragment on the client side.
  • Tiny Payload: Under 6.5KB raw / ~1.3KB gzipped. No Virtual DOM, no complex dependencies, no heavy SPA build tooling.

Features

  • Link Interception: Seamlessly upgrades standard <a> tags to partial frame swaps.
  • Form Interception: Intercepts both GET and POST forms (including button submitter values) automatically.
  • Smart Browser History: Updates window.history and document title for GET requests; treats non-GET mutations as stateless by default.
  • Element Syncing: Synchronize peripheral elements (like flash banners, shopping carts, or navigation menus) in a single navigation pass using data-frame-sync.
  • Server Override Control: Dynamic target overrides via the X-Target-Frame HTTP response header.
  • Loading & Error Attributes: Exposes declarative hooks (data-frame-loading, data-frame-submitting, data-frame-error) for effortless CSS styling.

Quick Start

  1. Include smolFrame.js in your document (or bundle it):
<script src="/js/smol-frame.js"></script>
  1. Mark your replacement region with data-frame and a unique id:
<main id="app-content" data-frame>
  <h1>Welcome to my app</h1>
  <a href="/admin/" data-frame-target="app-content">Manage CMS</a>
</main>

When clicked, smolFrame fetches /admin/, extracts the element matching #app-content from the returned HTML, replaces its inner content, updates the URL history, and updates the document title!

PHP/Composer

Library is also published on Packagist, with a PHP helper for getting the Javascript and minimal CSS. This may be helpful if you are bundling this library in a PHP project and wish to take advantage of Composer for versioning/updating it.

composer install joby/smol-frame
use Joby\Smol\Frame\SmolFrame;

// get a full path to the library's current version of the Javascript
SmolFrame::scriptFile();
// get the current raw Javascript content of the file
SmolFrame::scriptContent();

// get a full path to the library's current version of the minimal CSS file
SmolFrame::cssFile();
// get the current raw CSS content of the file
SmolFrame::cssContent();

HTML Attributes Reference

Attribute Applied To Description
data-frame Container Marks an element as an interceptable smolFrame target container.
data-frame-target="<id>" <a>, <form>, <button> Specifies the target container id to swap content into. Searches parent tree if omitted.
data-frame-target="_frame" <a>, <form>, <button> Targets the nearest parent container with a data-frame attribute.
data-frame-stateless Container Prevents pushState URL / history changes for requests targeting this frame.
data-frame-hide-missing Container Hides the container (display: none) if the response document does not contain a matching element.
data-frame-sync Any Element Keeps peripheral elements with matching ids in sync with the server response document.

Usage Examples

Basic Link Swapping

Target an explicit container by id:

<a href="/page/10" data-frame-target="main-frame">View page 10</a>

<div id="main-frame" data-frame>
  </div>

Targeting the Nearest Frame (_frame)

You can tell links or forms to swap their surrounding parent container without hardcoding an id:

<div id="card-42" data-frame>
  <p>Page status: Active</p>
  <a href="/pages/42/edit" data-frame-target="_frame">Edit</a>
</div>

Form Interception (GET & POST)

Forms targeting a frame are automatically intercepted:

<form action="/search" method="GET" data-frame-target="search-results">
  <input type="search" name="q" placeholder="Search..." autofocus />
  <button type="submit">Search</button>
</form>

<section id="search-results" data-frame>
  </section>
<form action="/pages/42/update" method="POST" data-frame-target="_frame">
  <input type="text" name="title" value="Current Title" />
  <button type="submit" name="action" value="save">Save Changes</button>
</form>

Note: Non-GET requests are automatically treated as stateless (they will not push history state).

Syncing Auxiliary Elements (data-frame-sync)

Sometimes a frame update should also refresh external widgets, such as a flash message notification area or a counter badge in a navbar. Add data-frame-sync and a matching id:

<div id="flash-area" data-frame-sync>
  </div>

<main id="content" data-frame>
  </main>

When navigating #content, smolFrame checks if the returned page contains #flash-area. If found, its inner content is updated in sync with the primary frame exchange.

Server Responses & Headers

Server Target Overrides (X-Target-Frame and X-Source-Frame)

Your server can dynamically instruct smolFrame to redirect output to a different container than the request originally targeted by sending the X-Target-Frame HTTP response header, and/or pull content from a different container in the response using X-Source-Frame:

HTTP/1.1 200 OK
Content-Type: text/html
X-Target-Frame: global-modal
X-Source-Frame: updated-modal-content

Identifying smolFrame Requests

Every fetch request sent by smolFrame includes the X-Smol-Frame header containing the target frame's ID:

X-Smol-Frame: main-frame

Styling Loading & Error States

During a frame exchange, smolFrame sets temporary dataset attributes on target elements. You can style these different states using pure CSS:

/* Styling frame loading states */
[data-frame][data-frame-loading] {
  opacity: 0.6;
  pointer-events: none;
  transition: opacity 0.15s ease;
}

/* Form submission styling */
[data-frame][data-frame-submitting] {
  cursor: wait;
}

/* Error banner styling */
[data-frame][data-frame-error]::before {
  content: "Error: " attr(data-frame-error);
  display: block;
  padding: 0.5rem 1rem;
  background: #fee2e2;
  color: #991b1b;
  border-radius: 4px;
  margin-bottom: 1rem;
}

Custom Events & Manual Actions

smol-frame:loaded

Dispatched on the target frame element after content has been fetched, parsed, and injected into the DOM.

document.addEventListener('smol-frame:loaded', (e) => {
  console.log('Frame loaded:', e.target, 'URL:', e.detail.url);
});

Manual Reload Trigger (smol-frame:reload)

You can trigger a frame to re-fetch its last known URL by dispatching a smol-frame:reload event anywhere inside or on the frame element:

const frame = document.getElementById('my-frame');
frame.dispatchEvent(new CustomEvent('smol-frame:reload', { bubbles: true }));

License

MIT License - See LICENSE file for details.